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

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

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

Создание веб-сайта с нуля на Django и Bootstrap. Статьи. Модель и администрирование

Сергей Серов

В командной строке создаем приложение с названием articles:

python manage.py startapp articles

Модель

Переходим в директорию приложения и заполняем файл models.py:

from django.db import models
from django.urls import reverse
from django.utils import timezone

from core.utils import slugify, strip_tags


class PublicArticlesManager(models.Manager):
    def get_queryset(self):
        return super(PublicArticlesManager, self).get_queryset().filter(published__lte=timezone.now())


class Article(models.Model):
    title = models.CharField('заголовок', max_length=255)
    slug = models.SlugField('слаг', max_length=255, blank=True, editable=False)
    text = models.TextField('текст')
    summary = models.CharField('резюме', max_length=255, blank=True, editable=False)
    published = models.DateTimeField('опубликована', blank=True, null=True, db_index=True)
    created = models.DateTimeField('создана', auto_now_add=True)
    updated = models.DateTimeField('обновлена', auto_now=True)

    objects = models.Manager()
    public = PublicArticlesManager()

    class Meta:
        verbose_name = 'статья'
        verbose_name_plural = 'статьи'
        ordering = ['-published']

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        self.slug = slugify(self.title, 255)
        # очищаем стрипом текст от всех тегов для резюме
        self.summary = strip_tags(self.text, 255)
        super(Article, self).save(*args, **kwargs)

    def get_absolute_url(self):
        return reverse('articles:article', args=[self.slug, self.pk])

    def get_admin_url(self):
        return reverse('admin:{}_{}_change'.format(self._meta.app_label, self._meta.model_name), args=[self.pk])

Слаг — это заголовок статьи, сформированный латинскими буквами. Используется для составления ссылок.

Краткая версия статьи используется для предоставления резюме в ленте последних публикаций.

Поле с датой публикации одновременно является и временем размещения статьи, и флагом. Например, если метка времени присутствует — значит статья опубликована, в противном же случае — является черновиком.

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

Сортировку модели задаем по дате публикации, последние статьи — сверху.

При сохранении модели, мы формируем слаг статьи вызывая функцию slugify, которая принимает два параметра: заголовок и максимальное допустимое количество символов на выходе.

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

Например, китайская пословица "会浮水的淹死,会骑马的摔死" состоящая всего лишь из 13 символов, при переводе обретет слаг "hui-fu-shui-de-yan-si-hui-qi-ma-de-shuai-si" что составит уже 43 символа.

Далее мы вызываем функцию strip_tags, передавая ей текст статьи. На выходе мы получим резюме публикации с ограничением в 255 знаков и без содержания каких-либо HTML тегов для эстетической составляющей.

Утилиты slugify и strip_tags мы опишем ниже.

Менеджер

PublicArticlesManager отсекает не только черновики, но и статьи, которые запланированы на определенные даты.

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

Утилиты

В каталоге core открываем utils.py и добавляем следующее:

import unidecode
import bleach

from django.utils.text import slugify as _slugify, Truncator


def slugify(value, truncate_chars):
    return Truncator(_slugify(unidecode.unidecode(value))).chars(truncate_chars, truncate='')


def strip_tags(value, truncate_chars):
    return Truncator(bleach.clean(value, tags=[], strip=True)).chars(truncate_chars)

Truncator как и _slugify это стандартные утилиты Django. Первая — ограничивает текст до заданного количество символов. Вторая — заменяет знаки для правильного составления слага, например, убирает знаки препинания. Unidecode занимается непосредственно переводом.

В функции strip_tags мы используем библиотеку bleach и конкретно утилиту clean, которая удаляет все HTML-теги из переданного текста.

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

Администрирование

Переходим к файлу admin.py и приводим к следующему виду:

from django.contrib import admin
from django.db import models

from ckeditor_uploader.widgets import CKEditorUploadingWidget

from .models import Article


@admin.register(Article)
class ArticleAdmin(admin.ModelAdmin):
    list_display = ['title', 'slug', 'published']
    list_filter = ['published']
    search_fields = ['title', 'slug']
    formfield_overrides = {models.TextField: {'widget': CKEditorUploadingWidget}}

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

В list_filter указываваем поля для быстрой фильтрации.

В списке search_fields содержится список полей, по которым производится поиск.

Далее в словаре formfield_overrides мы переопределяем виджет текстового поля. Заменяя его на CKEditorUploadingWidget.

Данный виджет представляет доступ к визуальному редактору CKEditor, который позволяет сосредоточиться на написании текста самой статьи, а не отвлекаться на HTML-теги.

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

Указываем читабельное название приложения редактируя файл apps.py:

from django.apps import AppConfig


class ArticlesConfig(AppConfig):
    name = 'articles'
    verbose_name = 'Статьи'

Настройки

Для внесения корректив в директории zero открываем settings.py и добавляем приложения articles, ckeditor и ckeditor_uploader:

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'ckeditor',
    'ckeditor_uploader',
    'core.apps.CoreConfig',
    'accounts.apps.AccountsConfig',
    'articles.apps.ArticlesConfig',
]

Далее мы добавляем настройки для корректной работы визуального редактора:

CKEDITOR_UPLOAD_PATH = 'uploads/'
CKEDITOR_IMAGE_BACKEND = 'pillow'

CKEDITOR_CONFIGS = {
    'default': {
        'skin': 'moono',
        'toolbar_Full': [
            ['Styles', 'Format', 'Bold', 'Italic', 'Underline', 'Strike', 'SpellChecker', 'Undo', 'Redo'],
            ['Link', 'Unlink', 'Anchor'],
            ['Image', 'Flash', 'Table', 'HorizontalRule'],
            ['TextColor', 'BGColor'],
            ['Smiley', 'SpecialChar'],
            ['Blockquote', 'BulletedList'],
            ['Source'], ['RemoveFormat'], ['Maximize'],
        ],
        'toolbar': 'Full',
        'width': '100%',
        'filebrowserWindowWidth': 940,
        'filebrowserWindowHeight': 725,
    },
}

Маршруты

Необходимо подключить маршруты для ckeditor, открываем urls.py и заменяем следующим:

from django.conf.urls import url, include
from django.contrib import admin
from django.conf.urls.static import static
from django.conf import settings

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'^ckeditor/', include('ckeditor_uploader.urls')),
    url(r'^accounts/', include('accounts.urls')),
] + 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)),
    ]

Миграция

Переходим в командную строку и выполняем команды для создания и применения данной миграции:

python manage.py makemigrations
python manage.py migrate