Здравствуйте, сегодня я хотел бы вам рассказать о том, как сделать модель, которая хранит в себе обычные страницы, а не отдельные записи в базе данных (для ListView, TemplateView и тд). Речь пойдёт о том, как расширить и дополнить существующие в Django flatpages. Но хотелось бы рассказать о проблеме, с которой я столкнулся и почему решил поделиться данным функционалом. Часто возникает ситуация, когда в админке для администратора сайта нужно реализовать функционал самой обычной страницы (одна запись в БД – это одна страница, где прописывается url, контент и доп. инфа для конкретной страницы). Тем самым можно создавать прямо из админки новые страницы с любым url и контентом.

Приведу пример: была поставлена задача реализовать такую страницу, где было бы обычное текстовое поле, к которому прикручен ckeditor и администратор мог бы менять и писать нужный текст о компании, а также переписывать его. Первая мысль, которая тебя посещает — это обычная запись в модели и в контроллере (вьюхе) сделать класс на основе TemplateView, и страница создана без проблем. Но далее начинаешь понимать, что это очень плохой стиль и если этот администратор начнёт добавлять новые записи в бд, то вёрстка поедет. Сразу же приходит в голову способ переопределить, например, метод get_context_data или get_queryset (в ListView). И там сделать нужную выборку (например, брать только самую первую запись из БД) где собственно администратор и будет править нужную страницу, но всё равно у него остаётся возможность добавлять новые записи в бд, которые просто будут игнорироваться. Придумав ещё пару способов, я отбросил эту затею. Посчитав это плохим тоном, я вспомнил о существование flatpages в Django, но освежив в памяти их функционал, стало ясно, что добавить что-то в их контекст данных невозможно, но можно, например, указать нужный шаблон, который обрабатывается ‘шаблонизатором’, но вот текст, который вы задаёте в поле текста, не обрабатывается ‘шаблонизатором’. Это стоит учитывать, но для меня это не являлось большой проблемой. Но вот что являлось, так это то что это изначально flatpages не расширяемый модуль и прикрутить столь важный ckeditor невозможно. Пришлось думать, как это можно реализовать. Но именно этот функционал мне и нужен был. После вводной части теперь давайте перейдём к более подробному изучению столь хорошему модулю flatpages, но к сожалению, неполноценному, давайте это исправим.

Что такое статичные страницы в джанге – это страницы содержимое которых не генерируется на основе хранящихся в модели данных, а задаётся в виде обычного html кода в соответствующих, заранее заданных, полях.

Основные недостатки:

  1. Данные не генерируются на основе данных из модели, а следовательно во views.py (далее буду называть контроллером по MVC, а не представление по MVT) мы не можем как то их обработать, дополнить и поместить в контекст данных что либо ещё. А также не можем изначально расширить или изменить модель (models.py).
  2. Содержимое таких страниц должно представлять собой чисты html код — это главная причина почему из коробки flatpages неполноценны. Так же стоит понимать, что данный код не обрабатывается ‘шаблонизатором’

Основные плюсы:

  1. Cодержимое flatpages включает в себя интернет-адрес страницы, её заголовок, содержимое и самое главное путь к файлу шаблона. Последний пункт, является ключевым, не смотря на то, что мы не можем передавать данные, но можем сделать нужную страницу используя: базовый шаблон, в котором у нас есть меню настроенное с помощью тегов и переменных джанги, задать места где нужно выводить данные из flatpages, подключать ‘включённые шаблоны’ и тд.
  2. Одна запись в модели соответствует одной странице на сайте, что является большим плюсом, система полностью настроена нужным образом, что позволяет не писать нам собственный велосипед.

Настройка проекта:


  1. В settings.py добавляем.

    INSTALLED_APPS = [
    …
        'django.contrib.sites', '''Служит для обеспечения работы нескольких web-сайтов на одной копии Django. В данном проекте, это нужно для корректной работы flatpages.'''
        'django.contrib.flatpages',
        'flatpage_main' #Потребуется в дальнейшем, имя можете задать любое.
    …
    ]
  2. Добавляем в проект(settings.py) SITE_ID = 1 и в MIDDLEWARE 'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
  3. Делаем синхронизацию с базой данных для добавление нужных таблиц в БД (до 5 шага 'flatpage_main' не писать в INSTALLED_APPS) .
  4. Заходим на административный сайт проверяем, что всё работает. У вас должна появиться графа ‘простые страницы’.
  5. Создаём новое приложение — python manage.py startapp 'flatpage_main'.
  6. Настраиваем ckeditor, на просторах интернета этот материал уже есть, думаю вы сможете его поставить и настроить, а использовать научитесь на этом примере. Если будет нужно, постараюсь написать статейку на хабр, но материал по этому вопросу есть в интернете.

Реализация поставленной задачи:


  1. Зайдём в модели и добавим следующий код —

    from django.db import models
    from django.contrib.flatpages.models import FlatPage
    from ckeditor_uploader.fields import RichTextUploadingField
    
    
    class NewFlatpage(models.Model):
        flatpage = models.OneToOneField(FlatPage)
        description = RichTextUploadingField(verbose_name = 'Основной текстовый контент страницы',default='')
        text_block = RichTextUploadingField(verbose_name='Дополнительный блок текста',default='')
    
        def __str__(self):
            return self.flatpage.title
    
        class Meta:
            verbose_name = "Содержание страницы"
            verbose_name_plural = "Содержание страницы"

    Разберёмся что тут написано:
    … import FlatPage Добавляем модель, на которую будем ссылаться.
    … import RichTextUploadingField спец поле, которое нужно для работы ckeditor

    В нашем новом классе ссылаемся через OneToOneField на модель FlatPage, тем самым создаём нужную связь, что позволяет нам увеличить базовую функциональность flatpages, расширяя её.
    А далее добавляем любые нужные нам поля, которые хотим, чтобы были в админке и в будущем на самой странице, тем самым можем добавить поле для любого нужного нам контента.
  2. Следующая ступень – это admin.py.

    from django.contrib import admin
    from django.contrib.flatpages.admin import FlatPageAdmin
    from .models import *
    
    
    class NewFlatpageInline(admin.StackedInline):
        model = NewFlatpage
        verbose_name = "Содержание"
    
    
    class FlatPageNewAdmin(FlatPageAdmin):
        inlines = [NewFlatpageInline]
        fieldsets = (
            (None, {'fields': ('url', 'title', 'sites')}),
            (('Advanced options'), {
                'fields': ('template_name',),
            }),
        )
        list_display = ('url', 'title')
        list_filter = ('sites', 'registration_required')
        search_fields = ('url', 'title')
    
    
    admin.site.unregister(FlatPage)
    admin.site.register(FlatPage, FlatPageNewAdmin)

    Приступим к разбору:

    Первый класс NewFlatpageInline и атрибут во втором классе inlines = [NewFlatpageInline], создаёт связь между этими классами, давая возможность на одной странице выводить поля из двух взаимосвязанных таблиц (‘позволяет редактировать связанные объекты на одной странице с родительским объектом’).

    fieldsets позволяет задать поля и настройки которые будут выведены в интерфейсе администратора у родительского объекта(flatpage), лишние настройки были убраны.

    Последние две строчки — это снятие и регистрация моделей в админке.
  3. Третий шаг это — urls.py.

     url(r'^main/$', views.flatpage, {'url': '/main/'}, name = 'main'),

    Добавляем эту строчку если чётко знаем что данная страница точно будет, и мы хотим обрабатывать её в базовом шаблоне, например, в меню, и ссылаться на неё по имени в переменной шаблона, потому что сам шаблон рендерится ‘шаблонизатором’.
    Для других страниц вы можете задать —
    url(r'^/', include('django.contrib.flatpages.urls')),

    Тем самым любой другой url адрес будет привязан к url адресу, который вы зададите при создание новой страницы.
  4. Четвёртый шаг это – template:

    {% url 'main' as main %}
    {% if request.path == main %}
        <li class="navigation__elem navigation__elem--main navigation__elem--current">
            <a href="{% url 'main'%}" class="navigation__href navigation__href--current"><i class="demo-icon icon-main__icon"></i>Главная</a></li>
    {% else %}
        <li class="navigation__elem nav-active"><a href="{% url 'main'%}" class="navigation__href"><i class="demo-icon icon-main__icon"></i>Главная</a></li>
    {% endif %}

    Выше написал пример работы меню с flatpages и остальными страницами основанных на контроллерах и шаблонах, думаю код тут предельно ясный и понятный, комментарии излишни. Как видим получить имя url адреса из urls.py, а потом обработать его не составляет никакого труда, тем самым закрывая последнюю сложность в реализации.

    Последний штрих, в шаблоне, который мы указали при создании страницы, при создании страницы в админке будет поле в которому указывается шаблон для этой страницы (напр: main.html). В этом шаблоне добавляем нужные нам поля, используя переменные

    {{flatpage.newflatpage.description|safe}}

    и

    {{flatpage.newflatpage.text_block|safe}}

    , тем самым позволяя нам вывести нужные данные на итоговую страницу. В этом шаблоне работает всё тоже самое что и в других, например, наследование от base.html.
  5. Деплойт – при деплойте вы столкнётесь с парой ошибок связанных с работой ‘django.contrib.sites', вы должны войти в шелл (python manage.py shell) и написать следующие команды:

    >>> from django.contrib.sites.models import Site
    >>> site = Site.objects.create(domain='http://ваш_домен.ru/', name=''http://ваш_домен.ru/)
    >>> site.save()
    

    Это должно помочь решить проблему с эксепшн.

На этом всё! Мы получили крутой и удобный способ добавлять, редактировать и удалять страницы на своём сайте. Научились реализовывать меню, которое позволит переключаться между активными и не активными пунктами и не важно flatpages это или страницы с контроллерами. Так же поработали с ckeditor и разобрались как его настраивать. Разобрали все попутные моменты и сложности. А самое главное соединили это всё в удобный и полезный инструмент, который позволит грамотно администрировать сайт в дальнейшем, а также развивать его, что конечно же удобно для конечного пользователя. Надеюсь данная статья была полезна и многим она поможет в работе! Кому-то сэкономит время. А кто-то напишет в комментариях ниже дополнительные советы и дополнит её!

Комментарии (14)


  1. mozzzg
    22.10.2017 23:03

    Могу посоветовать вам так же присмотреться к Django Fluent Pages и Django Fluent Contents.


    1. Anisov Автор
      22.10.2017 23:10

      Часть из них видел (такого же рода). Но они мне все не понравились, по тем или иным причинам. Решил сделать своё, под ту задачу которую описал выше). Но комментарий хороший, может кому-то пригодится, я об этом в статье решил не упоминать)


      1. mozzzg
        22.10.2017 23:57

        Но они мне все не понравились, по тем или иным причинам.

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

        Это так, просто размышления и крик души.

        C Django Fluent Pages и Django Fluent Contents у самого сложились очень сложные взаимоотношения, и всё же я плачу, колюсь, но кушаю кактус. В итоге не пойму, то ли так всё хорошо, что я продолжаю использовать, то ли так всё плохо, что плачу.


  1. vmm86
    23.10.2017 03:07

    Статья изложена не совсем человекопонятно, поэтому есть вопросы по стилю и по существу.
    1)

    модель, которая хранит в себе обычные страницы, а не отдельные записи в базе данных
    Почему содержимое "обычной страницы" не может быть "записью в базе данных"? -)

    2) Непонятно, в чём изначально была проблема.


    если этот администратор начнёт добавлять новые записи в бд, то вёрстка поедет
    Почему вёрстка будет разъезжаться, если добавляемый HTML-код всегда будет выводиться в заранее заготовленном для этого блоке шаблона, каждая страница — по своему псевдониму в URL?

    3) Зачем нужно было использовать именно связку flatpages с sites framework и с ещё одной кастомной моделью? Тем более, что flatpages изначально задуман для plain-text содержимого, о чём говорит само название. К тому же sites framework нет смысла использовать, а) если сайт у вас только один или б) если содержимое разных сайтов хранится в одной БД с одним файловым инстансом проекта.

    4) Наконец, зачем использовать для текстового содержимого
    RichTextUpliadingField, если это поле для загрузки файлов, а не для заполнения контентом?

    Я для подобной задачи просто создавал кастомную модель Article со служебными полями для названия (CharField), псевдонима страницы (SlugField), метатегов description и keywords (CharField) и самого HTML-содержимого (RichTextField из готового расширения django-ckeditor). Страницы редактировались в соотв. разделе админ-панели. Необходимости использовать sites framework со всем зависящим и от него дополнениями не было.


    1. Anisov Автор
      23.10.2017 04:35

      На некоторые из ваших вопросов у меня есть ответы в текcте, а на которые есть в офф. документации (на часть ваших вопросов сразу может ответить документация). Надеюсь следующее сообщение поможет вам разобраться)

      1. По поводу первого пункта(возможно не совсем корректно расписал, но так мне казалось будет понятнее для новичков), да у flatpages тоже будет отдельная запись в бд, но сразу со всеми данными, и самое главное, это то что не будет возможности на одну страницу добавить ещё данных из моделей (если создадим новый записи в базе данных, то наш шаблон в цикле их выведет на страницу, отсюда вёрстке ‘кирдык’, может так понятнее), как это делается в ListView, TemplateView.
      2. Если вы знакомы, например, с ListView, то любая запись, добавленная админом, через цикл фор будет добавлена на страницу. Пример кода: ({% for object in object_list %}). В самом начале я написал, что данное поведение можно изменить, сделав, например, в get_queryset выборку лишь первой записи из бд, то же самое и с TemplateView(но в другом методе). Но это плохой тон, и я считаю делать так абсолютное неправильно. Опять же в предисловии более грамотно описано почему.
      3. Без 'django.contrib.sites' flatpages не будет работать. См. документацию, см. мой пост, там я объяснил почему. У вас будет просто ошибка. Я усовершенствовал работу flatpages сделав нужную и полезную вещь для работы с простыми страницами, которые требует редактора контента для многих вот таких задач. “Cms в нано размере”. Ни где не написано, что расширять стандартный функционал джанги нельзя. Мне дали инструмент, я его улучшил) По-другому сделать никак нельзя, что бы одна модель (одна запись в бд) соответствовала одной странице. Лепить свой велосипед с нуля ещё хуже, а вот базовые классы не позволят этого сделать никак, только используя костыли в виде выборки из бд)
      4. 4По поводу этого пункта, для полной работы со всеми прелестями этого редактора, было выбрано именно это поле. Возможно понадобится загрузка файлов в будущем для админа. Это уже больше архитектурное замечание (оптимизация базы данных, скорости и тд), нежели замечание работоспособности. Тут уже кто как хочет, так и делает, поэтому я изначально отсылал людей самим установить ckeditor.

      Надеюсь я помог) Если у вас есть более лаконичное решение моей поставленной задачи, буду рад почитать на хабре:)


      1. vmm86
        24.10.2017 00:53

        По-другому сделать никак нельзя, чтобы одна запись в бд соответствовала одной странице… базовые классы не позволят этого сделать никак
        Можно взглянуть на это с другой стороны. Я легко решал подобную задачу обычным функциональным view. В модели Article одна запись — одна страница. Поле slug — уникальный псевдоним, по нахождению которого в URL в шаблоне рендерится именно эта конкретная страница. URL таких страниц не должны конфликтовать с другими URL проекта. Вот похожий пример на Тостере.

        Если очень хочется всегда использовать class-based views — как можно отобразить одну страницу с помощью, скажем, DetailView, написано на Хабре и на SO.


        1. Anisov Автор
          24.10.2017 09:04

          Отвечу по поводу DetailView, дабы не за блуждать людей, для этого базового класса нужно обязательно передавать pk в url, по которому будет искаться запись в базе данных(по этому я даже и не упоминал об этом классе в статье). По этому нет, такой способ не пройдёт, у вас будет ошибка. Ибо данный класс ждёт регулярку, в которой обрабатывается pk(ключ) и данный класс получает его с гет запросом, по которому ищет в дальнейшем запись. ( как пример из моего проекта — url(r'^blog/(?P\d+)$',..). А вот реализовать данный функционал, например, с помощью функции обработчика + те модули, которые написаны комментарием выше, это как раз можно, думаю, это как ещё одно элегантное решение, пока не испытывал, но можно, проблем не будет). Но в таком случае это лишь решение одной задачи. Тут же, я ещё упор делал на то, что админ может создавать сколько угодно страниц сам с различным контентом, а применение этому может быть широкое, вплоть до написания сайтов исключительно использующих данный функционал( такие сайты кстати есть, когда решал эту задачу, натыкался на статьи, где описывали крупные сайты, работающие исключительно на flatpages)
          Насчёт вашего решения, в принципе да, можно так, неплохой совет, тоже как вариант)


  1. Anisov Автор
    24.10.2017 09:24

    Я тут подумал, хотя если переопределить в DetailView атрибут slug_field методы get, get_queryset и обработать сложную регулярку из запроса(написанную в urls.py), и сделать её как pk, а в ней будет происходить обработка как раз всех адресов, то у нас может быть и получится вывести нужную страницу записав в одном из полей бд, поле, которое и будет отвечать за url адрес страницы, тем самым сделав одну запись. Но это я уже сам сейчас на ходу придумал) по этому наверняка не скажу реализацию, но попробовать потом можно) Я об это думал, когда разрабатывал данный метод и это тогда в голову не пришло тогда) А сейчас неожиданно пришёл в голову такой способ) Если вы что-то подобное имели введу, то расписали бы, так понятнее было бы, а так данный пост это пока что мои логические рассуждения, может кому-то поможет в дальнейшем такой вариант, который я сейчас предложил)


    1. vmm86
      24.10.2017 10:50

      Насколько я вижу в примере из актуальной документации к Django 1.11, DetailView вполне может работать с псевдонимом из URL без обязательного первичного ключа.
      Соответственно из URL может браться либо какое-то одно уникальное поле, однозначно идентифицирующее запись наряду с pk (например, псевдоним), либо комбинация полей, однозначно идентифицирующая запись в составе уникального ключа (например, дата/время/псевдоним).
      другим Возможно, в прошлых версиях было по-другому.



      1. Anisov Автор
        24.10.2017 14:11

        В частности, так и есть, но есть нюансы в реализации и в понимание. Советую тоже поработать с классами. Хотелось бы уточнить, я под pk имел введу, именно параметр — идентификатор. Это могут быть разные поля (соответственно разные регулярки), заданные либо pk_url_kwarg = 'pk', как первичный ключ, а в дальнейшем его можно преобразовать и изменить стандартную реализацию сортировки по бд(перехватить в методе ключ и изменить сортировку по любому полю), либо можно реализовать, так как вы предположили в теории, это реализуется с помощью slug_field(строка с именем поля модели, в котором хранится запись) + slug_url_kward. Забыл про эти атрибуты, посмотрел у себя в записях. Действительно реализовывать данную задачу можно по-разному. Но регулярка должна быть обязательно, которая передает значение, полученное из url адреса, в метод get. Ключ, по которому будет фильтроваться база данных в соответствие с параметром, должен быть. Сделать его можно разным (например, изменить работу pk, либо как написано выше) и задать по-разному, соответственно будет разная сложность. Но это все возможные варианты, больше нет. Я лично давно работаю с классами, всегда интересно практиковаться в них, функции уже давно не использую, считаю плохой практикой, и реализовывал сложные структуры с get, get_queryset, get_context_data, post.
        . Но в данном рассуждение, действительно лично даже я для себя смог выявить и придумать много решений, которые потом попробую, но это уже на отдельную статью клонит, детальный разбор реализации DetailView и нюансы в работе. Надеюсь вам и читателям понравятся ещё вот такие способы, который я сейчас придумал и надеюсь я их не слишком сложно расписал, и они понятные. Так что можно либо моим способом, которые я написал в комментарии выше, либо способом с slug_field + slug_url_kward. Думаю, эти варианты сработают, всё же я не тестировал пока, но думаю сработают. Конечно более глубокое описание, тестирование и выводы, это уже полноценная статья, которая пишется не быстро, с редактированием и вычитыванием.


      1. Anisov Автор
        24.10.2017 14:32

        Опят же то что вы написали не совсем корректно, работать можно не только через pk, но и через другие ключи(имя параметра, в котором передаётся интернет-адрес — в slug_url_kwarg, например), которые будут опираться на нужные поля в базе данных, нужное поле в бд, пишется в другом атрибуте. Так будет корректнее.


        1. vmm86
          24.10.2017 19:13

          работать можно не только через pk, но и через другие ключи
          Об этом, собственно, я и говорил.-)