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

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

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

Создание веб-сайта с нуля на Django и Bootstrap. Вопросы. Список вопросов и ответов к ним. Формы и утилиты

Сергей Серов

Самое интересное мы описали в прошлой статье — в моделях приложения. На десерт нам осталось разработать формы создания вопросов и ответов к ним, и разумеется составить представления, шаблоны, маршруты. По иронии — это самое объемное, что нам предстоит сделать в этом приложении.

Формы

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

from django import forms

from ckeditor.widgets import CKEditorWidget

from .models import Article, Question


class ArticleForm(forms.Form):
    text = forms.CharField(label='Текст', max_length=5000, widget=CKEditorWidget(config_name='questions'))

    def save(self, question, user, ip, commit=True):
        article = Article(
            question=question,
            text=self.cleaned_data['text'],
            author=user,
            author_ip=ip,
        )
        if commit:
            article.save()
        return article


class QuestionForm(ArticleForm):
    title = forms.CharField(label='Заголовок', max_length=150)

    field_order = ['title', 'text']

    def save(self, user, ip, *args, **kwargs):
        question = Question(title=self.cleaned_data['title'])
        question.save()
        article = super(QuestionForm, self).save(question, user, ip, commit=False)
        article.is_question = True
        article.save()

ArticleForm создана для создания ответов и содержит только одно поле — текст сообщения. Выставлено ограничение в пять тысяч символов и установлен специальный виджет, который отобразит визуальный редактор CKEditor. Сконфигурированный для приложения questions и разрешает только одобренные HTML-теги.

Метод save принимает объекты вопроса и пользователя, а также его IP. На основании этих данных и текста сообщения мы создаем запись в модели Article. Затем сохраняем ее если не указано обратное.

QuestionForm основано на первой форме, поскольку при создании вопроса нам также необходимо создать сообщение, которое будет содержать текст вопроса.

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

Создаем объект Question и вызываем стандартный метод, но без фиксации данных в базе данных, ведь нам еще надо указать, что данное сообщение является вопросом. Затем сохраняем запись Article.

Утилиты

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

import random
import hashlib
import time
import unidecode
import bleach

from django.conf import settings
from django.utils.text import slugify as _slugify, Truncator
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger


def get_random():
    try:  # по возможности используем системный рандом
        return random.SystemRandom()
    except NotImplementedError:
        random.seed(hashlib.sha256('{}{}{}'.format(
            random.getstate(), time.time(), settings.SECRET_KEY
        ).encode('utf-8')).digest())
        return random


def serializing_datetime(dt):
    dt = dt.isoformat()
    if dt.endswith('+00:00'):
        dt = dt[:-6] + 'Z'
    return dt


def get_domain():
    protocol = 'https' if settings.SITE_SSL else 'http'
    return '{}://{}'.format(protocol, settings.SITE_DOMAIN)


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)


def pagination(request, object_list, per_page):
    paginator = Paginator(object_list, per_page)
    try:
        objects = paginator.page(request.GET.get('page', 1))
    except (PageNotAnInteger, EmptyPage):
        objects = paginator.page(paginator.num_pages)
    return objects

Здесь мы добавили функцию pagination которая в свою очередь основана на стандартном классе Paginator.

Суть функции очень проста: вернуть объекты конкретно выбранный страницы, номер которой получен через GET-переменную.

GET-переменные в Django используются очень редко и это как раз тот случай, когда их использования считается оправданным.

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

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

Вот так выглядит маршрут списка, без указания какой-либо страницы:

/questions/

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

Далее рассмотрим это же представление, но с указанием конкретной страницы.

/questions/?page=17

Здесь мы через GET-переменную передаем номер и затем функция пытается найти данную страницу.

Если страница с подобным номером не найдена или вообще не является числом, то функция возвращает последнюю найденную страницу.

Например, некоторые гики могут не искать блок навигации, а просто указать в адресной строке в качестве номера страницы — текст, для того чтоб дать системе ясно понять: им необходима последняя страница.

/questions/?page=last

Настройки

Открываем файл настроек проекта settings.py и редактируем конфигурацию визуального редактора, добавляя коррективы для приложения questions:

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,
    },
    'questions': {
        'skin': 'moono',
        'width': '100%',
        'toolbar': 'Custom',
        'toolbar_Custom': [
            ['Bold', 'Italic'], ['Blockquote'], ['Undo', 'Redo'],
            ['RemoveFormat'], ['Maximize'],
        ]
    }
}

Затем добавляем настройки для представлений:

QUESTIONS_ITEMS_IN_FEED = 20  # кол-во вопросов в фиде
QUESTIONS_PER_PAGE = 20  # кол-во вопросов на страницу