Посібник по Django для початківців – Частина 3

40 хв. читання
15 листопада 2017

Основи

У цьому уроці ми глибоко зануримося у два основних поняття: URL-адреси та форми. В процесі ми вивчимо декілька інших концепцій, серед яких створення шаблонів багаторазового використання та встановлення сторонніх бібліотек. Ми також напишемо багато юніт-тестів.

Якщо ви слідуєте цій серії уроків з першої частини, покроково розробляючи проект і дотримуючись інструкцій, вам може знадобитись оновити models.py перед початком:

boards/models.py

class Topic(models.Model):
    # інші поля...
    # Додаємо `auto_now_add=True` у поле `last_updated`
    last_updated = models.DateTimeField(auto_now_add=True)

class Post(models.Model):
    # інші поля...
    # Додаємо `null=True` у поле `updated_by`
    updated_by = models.ForeignKey(User, null=True, related_name='+')

Тепер запустіть наступні команди з активованим virtualenv:

python manage.py makemigrations
python manage.py migrate

Якщо у вас вже є значення null=True в полі updated_by та auto_now_add=True в полі last_updated, ви можете безпечно ігнорувати вказівки вище.

Якщо ви віддаєте перевагу використанню мого вихідного коду в якості відправної точки, то можете взяти його на GitHub.

Поточний стан проекту можна знайти за тегом релізу v0.2-lw. Наведене нижче посилання приведе вас до потрібного місця:

https://github.com/sibtc/django-beginners-guide/tree/v0.2-lw

Розробка буде слідувати звідси.


URL-адреси

Продовжуючи розробку нашого застосунку, нам потрібно тепер реалізувати нову сторінку, щоб перерахувати всі теми, що належать до певної Дошки (Board). Нижче ви можете побачити каркас, який ми намалювали у попередньому уроці:

Посібник по Django для початківців – Частина 3
Рисунок 1: Каркас проекту Boards, який містить список усіх тем у дошці Django.

Ми почнемо з редагування urls.py всередині теки myproject:

myproject\urls.py

from django.conf.urls import url
from django.contrib import admin

from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^boards/(?P<pk>\\d+)/$', views.board_topics, name='board_topics'),
    url(r'^admin/', admin.site.urls),
]

На цей раз трохи відволічемося й проаналізуємо urlpatterns та url.

Диспетчер URL та URLconf (конфігурація URL) є основними частинами Django застосунку. Спочатку це може здаватися заплутаним.

Розробники Django працюють над пропозицією спрощення синтаксису маршрутизації. Але поки що, згідно з версією 1.11, це те, що ми маємо. Тому спробуємо зрозуміти, як все працює.

Проект може містити багато urls.py розподілених між застосунками. Але Django потребує urls.py, щоб використовувати його як відправну точку. Цей спеціальний urls.py називається root URLconf. Він визначається у файлі settings.py.

myproject/settings.py

ROOT_URLCONF = 'myproject.urls'

Він вже налаштований, тому тут нічого не потрібно змінювати.

Коли Django отримує запит, він починає шукати відповідність в URLconf проекту. Він починає з першого вводу змінної urlpatterns і перевіряє запитувану URL-адресу навпроти кожної введеної url.

Якщо Django знайде відповідність, він передасть запит до функції представлення, яка є другим параметром url. Порядок в urlpatterns має значення, тому що Django припиняє пошук, як тільки знаходить відповідність. Тепер, якщо Django не знайде відповідності в URLconf, він викличе виняток 404, що є кодом помилки для Page Not Found, тобто запитувана сторінка не була знайдена.

Це анатомія функції url:

def url(regex, view, kwargs=None, name=None):
    # ...
  • regex: регулярний вираз для зіставлення шаблонів URL у рядках. Відмітьте, що ці регулярні вирази не шукають параметри GET чи POST. У запиті http://127.0.0.1:8000/boards/?page=2 буде оброблена тільки /boards/.
  • view: функція представлення використовується для обробки запиту користувача для відповідної URL-адреси. Вона також приймає повернення функції django.conf.urls.include, яка використовується для посилання на зовнішній файл urls.py. Наприклад, ви можете використати її для визначення набору конкретних URL-адрес застосунку та включити його root URLconf, використовуючи префікс. Пізніше ми більше дізнаємося про цей концепт.
  • kwargs: аргументи довільного ключового слова, які передаються в цільове представлення. Зазвичай воно використовується для виконання простих налаштувань в багаторазових представленнях.
  • name: унікальний ідентифікатор для заданої URL. Це дуже важлива особливість. Завжди пам'ятайте давати імена вашим URL-адресам. Завдяки цьому ви зможете міняти конкретну URL у всьому проекті, просто змінюючи регулярний вираз. Тому важливо ніколи не писати URL-адреси у представленнях або шаблонах, і завжди посилатися на них за назвою.

Посібник по Django для початківців – Частина 3

Базові URL-адреси

Базові URL-адреси дуже просто створювати. Це лише питання відповідності рядків. Наприклад, скажімо, ми захотіли створити сторінку «about», її можна було б визначити наступним чином:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
]

Ми також можемо створити більш глибокі URL-структури:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
    url(r'^about/company/$', views.about_company, name='about_company'),
    url(r'^about/author/$', views.about_author, name='about_author'),
    url(r'^about/author/vitor/$', views.about_vitor, name='about_vitor'),
    url(r'^about/author/erica/$', views.about_erica, name='about_erica'),
    url(r'^privacy/$', views.privacy_policy, name='privacy_policy'),
]

Ось деякі приклади простої маршрутизації URL. Для всіх наведених вище прикладів функція представлення слідуватиме цій структурі:

def about(request):
    # тут щось робиться...
    return render(request, 'about.html')

def about_company(request):
    # тут теж щось робиться...
    # повернення деяких даних разом із представленням...
    return render(request, 'about_company.html', {'company_name': 'Simple Complex'})

Просунуті URL-адреси

Більш просунуте використання маршрутизації URL-адрес досягається завдяки використанню регулярного виразу для відповідності визначеним типам даних та створення динамічних URL-адрес.

Наприклад, щоб створити сторінку профілю, як це роблять багато сервісів, таких як facebook.com/codeguida або twitter.com/codeguida, де «codeguida» — ім'я користувача, ми можемо зробити наступне:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^(?P<username>[\\w.@+-]+)/$', views.user_profile, name='user_profile'),
]

Це відповідатиме всім дійсним іменам користувача для Django моделі User.

Тепер зверніть увагу на те, що наведений вище приклад є дуже дозвільною (permissive) URL-адресою. Це означає, що він відповідатиме багатьом шаблонам URL, оскільки визначається в кореневому каталозі URL без префіксу як, наприклад /profile/< username >/. У цьому випадку, якщо ми хочемо визначити URL-адресу з ім'ям / about /, нам слід визначити її перед шаблоном URL користувача:

from django.conf.urls import url
from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^about/$', views.about, name='about'),
    url(r'^(?P<username>[\\w.@+-]+)/$', views.user_profile, name='user_profile'),
]

Якщо сторінка «about» була визначена після шаблону URL-адреси користувача, Django ніколи не знайде його, тому що слово «about» буде відповідати регулярному виразу з ім'ям користувача, а представлення user_profile буде оброблене замість функції представлення about.

Існують деякі побічні ефекти. Наприклад, відтепер нам слід розглядати «about» як заборонене ім'я користувача, оскільки якщо користувач вибрав «about» в якості імені, він ніколи не побачить свою сторінку профілю.

Посібник по Django для початківців – Частина 3

  • До речі, якщо ви хочете створити класні URL для профілів користувачів, найлегшим рішенням для уникання суперечностей між URL, буде додавання префіксу, на кшталт /u/codeguida/, або, як робить Medium —/@codeguida, де @ є префіксом. Якщо ви не бажаєте взагалі використовувати префікс, подумайте про те, щоб використовувати список заборонених імен як тут: github.com/shouldbee/reserved-usernames. Або інший приклад — програма, яку я розробив, коли вивчав Django; В той час я створив свій власний список: github.com/vitorfs/parsifal/. Ці суперечності дуже розповсюджені. Візьміть GitHub наприклад; вони мають цю URL-адресу, щоб перераховувати всі репозиторії, які ви зараз переглядаєте: github.com/watching. Хтось зареєстрував ім'я користувача в GitHub з ім'ям «watching», тому він не може побачити свою сторінку профілю. Ми можемо побачити користувача з таким ім'ям, якщо спробуємо скористатись цією URL-адресою: github.com/watching/repositories, який мав би перераховувати репозиторії користувачів.

Ідея такого роду URL-маршрутизації полягає у створенні динамічних сторінок, де частина URL-адреси буде використовуватися як ідентифікатор певного ресурсу, який, у свою чергу, буде використовуватися для складання сторінки. Цей ідентифікатор може бути, наприклад, цілочисельним ID або рядком.

Спочатку ми працюватимемо з ідентифікатором Board, щоб створити динамічну сторінку для Topics. Повторімо знову приклад, який я дав на початку розділу URL:

url(r'^boards/(?P<pk>\\d+)/$', views.board_topics, name='board_topics')

Регулярний вираз \\d+ буде відповідати цілому числу довільного розміру. Це ціле число буде використано для вилучення Board з бази даних. Зауважте, що ми написали регулярний вираз як (?P<pk>\\d+), це говорить Django зафіксувати значення в аргументі ключового слова pk.

Ось як ми пишемо для нього функцію представлення:

def board_topics(request, pk):
    # тут щось робиться...

Оскільки ми використали регулярний вираз (?P<pk>\\d+), аргумент ключового слова у board_topics має бути названий pk.

Якщо ми хочемо використовувати будь-яке ім'я, реалізувати це можна було б так:

url (r'^boards/(\\d+)/$', views.board_topics, name = 'board_topics')

Функцію представлення можна визначити таким чином:

def board_topics (request, board_id):
     # щось робиться...

Або так:

def board_topics (request, id):
     # щось робиться...

Ім'я не має значення. Але краще використовувати іменовані параметри, тому що, коли ми починаємо складати більші URL-адреси, що охоплюють кілька ідентифікаторів і змінних, його простіше буде читати.

  • PK або ID? PK розшифровується як первинний ключ. Це найкоротший шлях для доступу до основного ключа моделі. Всі моделі Django мають цей атрибут. У більшості випадків використання властивостей pk збігається з id. Якщо ми не визначимо для моделі первинний ключ, Django автоматично створить AutoField з назвою id, який стане її первинним ключем. Якщо ви визначили інший первинний ключ, скажімо, поле email, то для доступу до нього ви можете використовувати obj.email або obj.pk.

Використання API URL-адрес

Настав час для написання коду. Реалізуймо сторінку переліку тем (див. рис. 1), про яку я згадував на початку розділу URL.

По-перше, відредагуйте urls.py, додавши наш новий маршрут URL:

myproject/urls.py

from django.conf.urls import url
from django.contrib import admin

from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^boards/(?P<pk>\\d+)/$', views.board_topics, name='board_topics'),
    url(r'^admin/', admin.site.urls),
]

Тепер створімо функцію представлення board_topics:

boards/views.py

from django.shortcuts import render
from .models import Board

def home(request):
    # код обмежений для стислості

def board_topics(request, pk):
    board = Board.objects.get(pk=pk)
    return render(request, 'topics.html', {'board': board})

У теці templates створимо новий шаблон, який назвемо topics.html:

templates/topics.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{{ board.name }}</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item">Boards</li>
        <li class="breadcrumb-item active">{{ board.name }}</li>
      </ol>
    </div>
  </body>
</html>
  • Поки що ми просто створюємо нові HTML-шаблони. Не хвилюйтесь, в наступному розділі я покажу вам як створювати шаблони багаторазового використання.

Тепер перевірте URL http://127.0.0.1:8000/boards/1/ у браузері. Результатом має бути наступна сторінка:

Посібник по Django для початківців – Частина 3

А тепер саме час для написання тестів! Відредагуйте файл tests.py, додавши наступні тести у кінець файлу:

boards/tests.py

from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import home, board_topics
from .models import Board

class HomeTests(TestCase):
    # ...

class BoardTopicsTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')

    def test_board_topics_view_success_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_board_topics_view_not_found_status_code(self):
        url = reverse('board_topics', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_board_topics_url_resolves_board_topics_view(self):
        view = resolve('/boards/1/')
        self.assertEquals(view.func, board_topics)

Тут слід відзначити кілька речей. Цього разу ми використовували метод setUp. У цьому методі ми створили екземпляр Board для використання в тестах. Ми повинні це робити, тому що набір тестування Django не виконує тестування з поточною базою даних. Для запуску тестів Django «на льоту» створює нову базу даних, застосовує всі міграції моделі, запускає тести, а після завершення, знищує базу тестування.

Отже, в методі setUp ми готуємо середовище для виконання тестів, щоб імітувати сценарій.

  • Метод test_board_topics_view_success_status_code: тестує, якщо Django повертає код стану 200 (успіх) для чинної Board.
  • Метод test_board_topics_view_not_found_status_code: тестує, якщо Django повертає код статусу 404 (сторінку не знайдено) для Board, яка не існує в базі даних.
  • Метод test_board_topics_url_resolves_board_topics_view: перевіряє, чи використовує Django правильну функцію перегляду, щоб відобразити теми.

Настав час провести тести:

python manage.py test

І вивід:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.E...
======================================================================
ERROR: test_board_topics_view_not_found_status_code (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...
boards.models.DoesNotExist: Board matching query does not exist.

----------------------------------------------------------------------
Ran 5 tests in 0.093s

FAILED (errors=1)
Destroying test database for alias 'default'...

Помилка test_board_topics_view_not_found_status_code. Ми можемо побачити в Traceback, що він повернув виняток «boards.models.DoesNotExist:запит на відповідність дошки не існує».

Посібник по Django для початківців – Частина 3

У продакшені з DEBUG=False відвідувач побачить сторінку 500 Internal Server Error. Але це не та поведінка, яка нам потрібна.

Ми хочемо показати 404 Page Not Found. Отже, переробімо наше представлення:

boards/views.py

from django.shortcuts import render
from django.http import Http404
from .models import Board

def home(request):
    # код обмежений для стислості

def board_topics(request, pk):
    try:
        board = Board.objects.get(pk=pk)
    except Board.DoesNotExist:
        raise Http404
    return render(request, 'topics.html', {'board': board})

Спробуємо ще раз:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.042s

OK
Destroying test database for alias 'default'...

Ура! Зараз все працює як очікувалось.

Посібник по Django для початківців – Частина 3

Це стандартна сторінка, яку показує Django, коли DEBUG=False. Пізніше ми можемо налаштувати сторінку 404 на відображення чогось іншого.

Зараз це дуже розповсюджений варіант використання. Django має короткий шлях, щоб спробувати отримати об'єкт, або повернути 404, якщо об'єкт не існує.

Тому давайте знову перебудуємо представлення board_topics:

from django.shortcuts import render, get_object_or_404
from .models import Board

def home(request):
    # код обмежений для стислості

def board_topics(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'topics.html', {'board': board})

Змінили код? Протестуйте його:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.....
----------------------------------------------------------------------
Ran 5 tests in 0.052s

OK
Destroying test database for alias 'default'...

Нічого не зламали. Можемо продовжувати розробку.

Наступним кроком буде створення навігаційних посилань на екранах. На домашній сторінці повинне бути посилання для запрошення відвідувача на сторінку тем обраної Board. Аналогічно, сторінка тем повинна містити посилання назад на домашню сторінку.

Посібник по Django для початківців – Частина 3

Можемо почати з написання тестів для класу HomeTests:

boards/tests.py

class HomeTests(TestCase):
    def setUp(self):
        self.board = Board.objects.create(name='Django', description='Django board.')
        url = reverse('home')
        self.response = self.client.get(url)

    def test_home_view_status_code(self):
        self.assertEquals(self.response.status_code, 200)

    def test_home_url_resolves_home_view(self):
        view = resolve('/')
        self.assertEquals(view.func, home)

    def test_home_view_contains_link_to_topics_page(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': self.board.pk})
        self.assertContains(self.response, 'href="{0}"'.format(board_topics_url))

Зауважте, що тепер ми додали метод setUp також і для HomeTests. Це тому, що тепер нам знадобиться екземпляр Board, також ми перенесли url і response у setUp, щоб ми могли повторно використовувати ту саму відповідь у новому тесті.

Новий тест тут — це test_home_view_contains_link_to_topicspage. Ми використовуємо метод assertContains, щоб перевірити, чи містить тіло відповіді даний текст. Текст, який ми використовуємо в тесті, — це частина href тегу a. Отже, ми в основному перевіряємо, чи має тіло відповіді текст href="/boards/1/".

Повернімося до тестів:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....F.
======================================================================
FAIL: test_home_view_contains_link_to_topics_page (boards.tests.HomeTests)
----------------------------------------------------------------------
# ...

AssertionError: False is not true : Couldn't find 'href="/boards/1/"' in response

----------------------------------------------------------------------
Ran 6 tests in 0.034s

FAILED (failures=1)
Destroying test database for alias 'default'...

Тепер ми напишемо код, який змусить цей тест пройти.

Відредагуйте шаблон home.html:

templates/home.html

<!-- код обмежений для стислості -->
<tbody>
  {% for board in boards %}
    <tr>
      <td>
        <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
        <small class="text-muted d-block">{{ board.description }}</small>
      </td>
      <td class="align-middle">0</td>
      <td class="align-middle">0</td>
      <td></td>
    </tr>
  {% endfor %}
</tbody>
<!-- код обмежений для стислості -->

Отже, в основному ми змінили рядок:

{{ board.name }}

На:

<a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>

Завжди використовуйте тег шаблону {% url %} для складання URL-адрес застосунків. Перший параметр — це назва URL (визначена в URLconf, тобто urls.py), після нього ви можете передавати довільну кількість аргументів за потребою.

Якби це була проста URL-адреса, як-от домашня сторінка, це було б просто {% url 'home'%}.

Збережіть файл і знову запустіть тести.

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
......
----------------------------------------------------------------------
Ran 6 tests in 0.037s

OK
Destroying test database for alias 'default'...

Добре! Можемо перевірити як це виглядає в браузері:

Посібник по Django для початківців – Частина 3

Тепер зворотнє посилання. Спершу ми можемо написати тест:

boards/tests.py

class BoardTopicsTests(TestCase):
    # код обмежений для стислості...

    def test_board_topics_view_contains_link_back_to_homepage(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(board_topics_url)
        homepage_url = reverse('home')
        self.assertContains(response, 'href="{0}"'.format(homepage_url))

Запустимо їх:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.F.....
======================================================================
FAIL: test_board_topics_view_contains_link_back_to_homepage (boards.tests.BoardTopicsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
# ...

AssertionError: False is not true : Couldn't find 'href="/"' in response

----------------------------------------------------------------------
Ran 7 tests in 0.054s

FAILED (failures=1)
Destroying test database for alias 'default'...

Оновимо шаблон тем дошки:

templates/topics.html

{% load static %}<!DOCTYPE html>
<html>
  <head><!-- код обмежений для стислості --></head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
        <li class="breadcrumb-item active">{{ board.name }}</li>
      </ol>
    </div>
  </body>
</html>

Запустимо тести:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.061s

OK
Destroying test database for alias 'default'...

Посібник по Django для початківців – Частина 3

Як я вже згадував раніше, маршрутизація URL-адрес є фундаментально частиною веб-застосунку. Завдяки цим знанням ми зможемо продовжити розробку. Далі, щоб завершити розділ про URL-адреси, ви знайдете підсумок корисних патернів URL.

Список корисних патернів URL

Хитра частина — це регулярний вираз. Тому я склав список найбільш часто використовуваних патернів URL. Ви завжди можете звернутися до цього списку, коли вам потрібна певна URL-адреса.

AutoField первинного ключа

Регулярний вираз (?P<pk>\\d+)
Приклад url(r'^questions/(?P<pk>\\d+)/$', views.question, name=' question ')
Дійсна URL /questions/934/
Збір даних {'pk': ' 934 '}

Порожнє поле

Регулярний вираз (?P<slug>[-\\w]+)
Приклад url(r'^posts/(?P<slug>[-\\w]+)/$', views.post, name='post')
Дійсна URL /posts/hello-world/
Збір даних {'slug': 'hello-world'}

Порожнє поле з первинним ключем

Регулярний вираз (?P<slug>[-\\w]+)-(?P<pk>\\d+)
Приклад url(r'^blog/(?P<slug>[-\\w]+)-(?P<pk>\\d+)/$', views.blog_post, name='blog_post')
Дійсна URL /blog/hello-world-159/
Збір даних {'slug': 'hello-world', 'pk': '159'}

Ім'я користувача Django

Регулярний вираз (?P<username>[\\w.@+-]+)
Приклад url(r'^profile/(?P<username>[\\w.@+-]+)/$', views.user_profile, name='user_profile')
Дійсна URL /profile/vitorfs/
Збір даних {'username': 'vitorfs'}

Рік

Регулярний вираз (?P<year>[0-9]{4})
Приклад url(r'^articles/(?P<year>[0-9]{4})/$', views.year_archive, name='year')
Дійсна URL /articles/2016/
Збір даних {'year': '2016'}

Рік/Місяць

Регулярний вираз (?P<year>[0-9]{4})/(?P<month>[0-9]{2})
Приклад url(r'^articles/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})/$', views.month_archive, name='month')
Дійсна URL /articles/2016/01/
Збір даних {'year': '2016', 'month': '01'}

Більше деталей про ці патерни ви можете знайти в цьому пості: Список корисних патернів URL


Шаблони багаторазового використання

До цього часу ми копіювали та вставляли HTML, повторюючи окремі частини HTML документа, що в довгостроковій перспективі може призвести до проблем.

У цьому розділі ми збираємось переробити наші шаблони HTML, створивши головну сторінку (master page) та лише додавши унікальну частину до кожного шаблону.

Створіть новий файл з ім'ям base.html в теці templates:

templates/base.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Django Boards{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>
    <div class="container">
      <ol class="breadcrumb my-4">
        {% block breadcrumb %}
        {% endblock %}
      </ol>
      {% block content %}
      {% endblock %}
    </div>
  </body>
</html>

Це буде наша головна сторінка. Кожен шаблон, який ми створюємо, розширюватиме цей спеціальний шаблон. Зверніть увагу на те, що ми ввели тег {% block %}. Він використовується для резервування простору в шаблоні, а «дочірній» шаблон (який розширює головну сторінку) може вставляти код і HTML у межах цього простору.

У випадку {% block title %} ми також встановлюємо значення за замовчуванням, яке є «Django Boards». Воно буде використане, якщо ми не встановимо значення для {% block title %} у дочірньому шаблоні.

Переробімо тепер два наших шаблони: home.html та topics.html.

templates/home.html

{% extends 'base.html' %}

{% block breadcrumb %}
  <li class="breadcrumb-item active">Boards</li>
{% endblock %}

{% block content %}
  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>Board</th>
        <th>Posts</th>
        <th>Topics</th>
        <th>Last Post</th>
      </tr>
    </thead>
    <tbody>
      {% for board in boards %}
        <tr>
          <td>
            <a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a>
            <small class="text-muted d-block">{{ board.description }}</small>
          </td>
          <td class="align-middle">0</td>
          <td class="align-middle">0</td>
          <td></td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
{% endblock %}

Перший рядок у шаблоні home.html{% extends 'base.html' %}. Тег говорить Django використовувати шаблон base.html як головну сторінку. Після цього ми використовуємо блоки для розміщення унікального вмісту сторінки.

templates/topics.html

{% extends 'base.html' %}

{% block title %}
  {{ board.name }} - {{ block.super }}
{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item active">{{ board.name }}</li>
{% endblock %}

{% block content %}
    <!-- наразі просто залиште це місце пустим. ми згодом додамо сюди ядро -->
{% endblock %}

У шаблоні topics.html ми змінюємо значення за замовчуванням {% block title %}. Зверніть увагу, що ми можемо повторно використовувати значення за замовчуванням блоку, викликавши {{ block.super }}. Тут ми граємося з назвою сайту, яку ми визначили в base.html як «Django ». Таким чином, для сторінки дошки «Python» заголовок буде «Python - Django Boards», для дошки «Random» назва буде «Random - Django Boards».

Тепер запустимо тести й перевіримо, чи нічого ми не зламали:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.......
----------------------------------------------------------------------
Ran 7 tests in 0.067s

OK
Destroying test database for alias 'default'...

Добре! Все виглядає як треба.

Тепер, коли у нас є шаблон base.html, ми можемо легко додати верхню панель із меню:

templates/base.html

{% load static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Django Boards{% endblock %}</title>
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
  </head>
  <body>

    <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
      <div class="container">
        <a class="navbar-brand" href="{% url 'home' %}">Django Boards</a>
      </div>
    </nav>

    <div class="container">
      <ol class="breadcrumb my-4">
        {% block breadcrumb %}
        {% endblock %}
      </ol>
      {% block content %}
      {% endblock %}
    </div>
  </body>
</html>

Посібник по Django для початківців – Частина 3 Посібник по Django для початківців – Частина 3

HTML, який я використовував, є частиною Bootstrap 4 Navbar Component.

Приємним доповненням, яке я хотів би додати, є зміна шрифту в логотипі ( .navbar-brand ) сторінки.

Перейдіть на сторінку fonts.google.com, введіть «Django Boards» або будь-яке інше ім'я, яке ви дали своєму проекту, після чого натисніть apply to all fonts. Прогляньте, знайдіть той, що вам подобається.

Посібник по Django для початківців – Частина 3

Додайте шрифт у шаблон base.html:

static %}<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>{% block title %}Django Boards{% endblock %}</title>
    <link href="https://fonts.googleapis.com/css?family=Peralta" rel="stylesheet">
    <link rel="stylesheet" href="{% static 'css/bootstrap.min.css' %}">
    <link rel="stylesheet" href="{% static 'css/app.css' %}">
  </head>
  <body>
    <!-- код обмежений для стислості -->
  </body>
</html>

Тепер всередині теки static/css створіть новий CSS файл з назвою app.css:

static/css/app.css

.navbar-brand {
  font-family: 'Peralta', cursive;
}

Посібник по Django для початківців – Частина 3


Форми

Форми використовуються для обробки вводу користувача. Це дуже поширена задача в будь-якому веб-застосунку або сайті. Стандартний спосіб це зробити — через HTML-форми, де користувач вводить деякі дані, відправляє їх на сервер, а потім сервер з ним щось робить.

Посібник по Django для початківців – Частина 3

Обробка форми — досить складна задача, оскільки вона передбачає взаємодію з багатьма рівнями застосунку. Існує багато питань, про які слід попіклуватися. Наприклад, усі дані, які надходять на сервер, представлені у рядковому форматі, тому нам треба перетворити його в правильний тип даних (integer, float, date тощо) перш, ніж почати з ними щось робити. Ми повинні перевірити дані бізнес-логіки застосунку. Нам також треба ретельно очищати дані, щоб уникнути проблем безпеки, таких як SQL Injection та атаки XSS.

Хороша новина полягає в тому, що Django Forms API робить весь процес набагато простішим, автоматизуючи добрий шмат цієї роботи. Крім того, кінцевий результат — набагато більш безпечний код, ніж більшість програмістів могли б реалізувати самостійно. Таким чином, незалежно від того, наскільки проста HTML форма, завжди використовуйте Forms API.

Як не треба реалізовувати форму

Спочатку я думав одразу перейти до API форм. Але непогано було б провести деякий час намагаючись зрозуміти деталі, які лежать в основі обробки форм. Інакше це все буде виглядати як магія, що погано, тому що, коли щось піде не так, ви не будете знати, де шукати проблему.

З більш глибоким розумінням деяких концепцій програмування ми можемо більше контролювати ситуацію. А бути в змозі контролювати ситуацію — дуже важливо, оскільки це дасть нам можливість писати код з більшою впевненістю. Коли ми точно знаємо, що відбувається, набагато простіше реалізувати код передбачуваної поведінки. Також стає набагато легше дебажити код та знаходити помилки, оскільки ви знаєте, де шукати.

У будь-якому випадку, почнімо з реалізації наступної форми:

Посібник по Django для початківців – Частина 3

Це один із каркасів, які ми намалювали в минулому уроці. Тепер я розумію, що це може бути невдалим прикладом для початку, тому що ця конкретна форма включає обробку даних двох різних моделей: Topic (тема) та Post (повідомлення).

Є ще один важливий аспект, який ми досі не розглянули — автентифікація користувача. Ми повинні показувати цей екран тільки автентифікованим користувачам. Таким чином, ми можемо сказати, хто створив Topic або Post.

Отже, давайте зараз упустимо деякі подробиці й зосередимось на тому, як зберегти ввід користувача в базі даних.

Спершу створімо новий URL маршрут під назвою new_topic:

myproject/urls.py

from django.conf.urls import url
from django.contrib import admin

from boards import views

urlpatterns = [
    url(r'^$', views.home, name='home'),
    url(r'^boards/(?P<pk>\\d+)/$', views.board_topics, name='board_topics'),
    url(r'^boards/(?P<pk>\\d+)/new/$', views.new_topic, name='new_topic'),
    url(r'^admin/', admin.site.urls),
]

Те, як ми створюємо URL, допоможе нам визначити правильну Board.

Тепер створімо функцію представлення new_topic:

boards/views.py

from django.shortcuts import render, get_object_or_404
from .models import Board

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)
    return render(request, 'new_topic.html', {'board': board})

На даний момент функція представлення new_topic виглядає точно так само, як board_topics. Це зроблено спеціально, робімо один крок за раз.

Тепер нам просто потрібен шаблон з назвою new_topic.html, щоб побачити як працює код:

templates/new_topic.html

{% extends 'base.html' %}

{% block title %}Start a New Topic{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
  <li class="breadcrumb-item active">New topic</li>
{% endblock %}

{% block content %}

{% endblock %}

Наразі у нас є рядок, який забезпечує навігацію. Зверніть увагу, що ми включили URL до представлення board_topics.

Відкрийте URL http://127.0.0.1:8000/boards/1/new/. Результатом, на даний момент, є наступна сторінка:

Посібник по Django для початківців – Частина 3

Ми досі не реалізували спосіб доступу до цієї нової сторінки, але, якщо ми змінимо URL-адресу на http://127.0.0.1:8000/boards/2/new/, це повинно привести нас до Python Board:

Посібник по Django для початківців – Частина 3

  • Результат може відрізнятися для вас, якщо ви не виконали кроки з попереднього уроку. У моєму випадку, в базі даних три екземпляри Board: Django = 1, Python = 2 і Random = 3. Ці цифри — це ID з бази даних, які використовуються з URL для визначення потрібного ресурсу.

Ми вже можемо додати деякі тести:

boards/tests.py

from django.core.urlresolvers import reverse
from django.urls import resolve
from django.test import TestCase
from .views import home, board_topics, new_topic
from .models import Board

class HomeTests(TestCase):
    # ...

class BoardTopicsTests(TestCase):
    # ...

class NewTopicTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')

    def test_new_topic_view_success_status_code(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 200)

    def test_new_topic_view_not_found_status_code(self):
        url = reverse('new_topic', kwargs={'pk': 99})
        response = self.client.get(url)
        self.assertEquals(response.status_code, 404)

    def test_new_topic_url_resolves_new_topic_view(self):
        view = resolve('/boards/1/new/')
        self.assertEquals(view.func, new_topic)

    def test_new_topic_view_contains_link_back_to_board_topics_view(self):
        new_topic_url = reverse('new_topic', kwargs={'pk': 1})
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        response = self.client.get(new_topic_url)
        self.assertContains(response, 'href="{0}"'.format(board_topics_url))

Короткий огляд тестів нашого нового класу NewTopicTests:

  • setUp: створює екземпляр Board, який буде використовуватися під час тестів
  • test_new_topic_view_success_status_code: перевіряє, чи успішно виконано запит до представлення
  • test_new_topic_view_not_found_status_code: перевіряє, чи викликає представлення помилку 404, коли Board не існує
  • test_new_topic_url_resolves_new_topic_view: перевіряє, чи використовується правильне представлення
  • test_new_topic_view_contains_link_back_to_board_topics_view: забезпечує перехід назад до списку тем

Запустіть тести:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...........
----------------------------------------------------------------------
Ran 11 tests in 0.076s

OK
Destroying test database for alias 'default'...

Добре, настав час створити форму.

templates/new_topic.html

{% extends 'base.html' %}

{% block title %}Start a New Topic{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
  <li class="breadcrumb-item active">New topic</li>
{% endblock %}

{% block content %}
  <form method="post">
    {% csrf_token %}
    <div class="form-group">
      <label for="id_subject">Subject</label>
      <input type="text" class="form-control" id="id_subject" name="subject">
    </div>
    <div class="form-group">
      <label for="id_message">Message</label>
      <textarea class="form-control" id="id_message" name="message" rows="5"></textarea>
    </div>
    <button type="submit" class="btn btn-success">Post</button>
  </form>
{% endblock %}

Це необроблена HTML-форма, створена вручну за допомогою класів CSS, наданих Bootstrap 4. Вона виглядає так:

Посібник по Django для початківців – Частина 3

Для тегу <form> ми повинні визначити атрибут method. Це вказує браузеру на те, як ми хочемо спілкуватися з сервером. Специфікація HTTP визначає кілька методів запиту (дієслова). Але найчастіше ми використовуватимемо GET та POST запити.

GET — найпоширеніший тип запиту. Він використовується для отримання даних з сервера. Кожного разу, коли ви натискаєте на посилання або вводите URL безпосередньо в браузер, ви створюєте запит GET.

POST використовується, коли ми хочемо змінити дані на сервері. Отже, кажучи загалом, кожен раз, коли ми надсилаємо дані на сервер, які призведуть до зміни стану ресурсу, ми завжди повинні надсилати їх за допомогою запиту POST.

Django захищає всі запити POST, використовуючи токен CSRF (Cross-Site Request Forgery). Це міра безпеки, щоб запобігти зовнішнім сайтам або застосункам передавати дані у наш застосунок. Щоразу, коли застосунок отримуватиме POST, він спочатку шукатиме токен CSRF. Якщо запит не має токену, або токен недійсний, він відхилить розміщені дані.

Результат тегу шаблону csrf_token:

{% csrf_token %}

Це приховане поле, яке відправляється разом з іншими даними форми:

<input type="hidden" name="csrfmiddlewaretoken" value="jG2o6aWj65YGaqzCpl0TYTg5jn6SctjzRZ9KmluifVx0IVaxlwh97YarZKs54Y32">

Іще одна річ — ми повинні встановити назву входів HTML. Ім'я буде використовуватися для отримання даних на стороні сервера.

<input type="text" class="form-control" id="id_subject" name="subject">
<textarea class="form-control" id="id_message" name="message" rows="5"></textarea>

Ось як ми отримуємо дані:

subject = request.POST['subject']
message = request.POST['message']

Таким чином, наївна реалізація представлення, яке захоплює дані з HTML та запускає нову тему, може бути написана так:

from django.contrib.auth.models import User
from django.shortcuts import render, redirect, get_object_or_404
from .models import Board, Topic, Post

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)

    if request.method == 'POST':
        subject = request.POST['subject']
        message = request.POST['message']

        user = User.objects.first()  # TODO: get the currently logged in user

        topic = Topic.objects.create(
            subject=subject,
            board=board,
            starter=user
        )

        post = Post.objects.create(
            message=message,
            topic=topic,
            created_by=user
        )

        return redirect('board_topics', pk=board.pk)  # TODO: редірект на створену сторінку теми

    return render(request, 'new_topic.html', {'board': board})

Представлення розглядає лише щасливий шлях (happy path), який полягає в отриманні даних та зберіганні їх у базі даних. Але є деякі відсутні частини. Ми не перевіряємо дані. Користувач міг відправити порожню форму чи тему (subject), яка перевищує 255 символів.

Ми захардкодили поля User, тому що ще не реалізували автентифікацію. Але існує легкий спосіб ідентифікувати залогіненого користувача. Ми до цього доберемося у наступному уроці. Також у нас не реалізоване представлення, де ми перерахуємо всі публікації в межах теми, тому в разі успіху ми перенаправляємо користувача на сторінку, де ми перераховуємо всі теми форуму.

Посібник по Django для початківців – Частина 3

Відправити форму можна, натиснувши кнопку Post:

Посібник по Django для початківців – Частина 3

Схоже, що це спрацювало. Але ми ще не реалізували перелік тем, тому тут поки нема на що дивитися. Відредагуймо файл templates/topics.html, щоб зробити правильний перелік:

templates/topics.html

{% extends 'base.html' %}

{% block title %}
  {{ board.name }} - {{ block.super }}
{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item active">{{ board.name }}</li>
{% endblock %}

{% block content %}
  <table class="table">
    <thead class="thead-inverse">
      <tr>
        <th>Topic</th>
        <th>Starter</th>
        <th>Replies</th>
        <th>Views</th>
        <th>Last Update</th>
      </tr>
    </thead>
    <tbody>
      {% for topic in board.topics.all %}
        <tr>
          <td>{{ topic.subject }}</td>
          <td>{{ topic.starter.username }}</td>
          <td>0</td>
          <td>0</td>
          <td>{{ topic.last_updated }}</td>
        </tr>
      {% endfor %}
    </tbody>
  </table>
{% endblock %}

Посібник по Django для початківців – Частина 3

Ага! Створена нами тема тут є.

Тут є дві нові концепції:

Ми вперше використовуємо властивості topics у моделі Board. Ці властивості створюються Django автоматично за допомогою зворотного зв'язку. На попередніх кроках ми створили екземпляр Topic:

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)

    # ...

    topic = Topic.objects.create(
        subject=subject,
        board=board,
        starter=user
    )

У рядку board = board ми встановили поле board в модель TopicForeignKey(Board). Таким чином, наш екземпляр Board тепер знає, що він має екземпляр Topic, пов'язаний з ним.

Причина, чому ми використали board.topics.all замість того, щоб тільки використати board.topics, в тому, що board.topicsменеджер пов'язаних об'єктів (Related Manager), який дуже схожий на Model Manager, зазвичай доступний у властивості board.objects. Отже, щоб повернути всі теми, пов'язані з даною дошкою, нам треба запустити board.topics.all(). А для того, щоб відфільтрувати деякі дані, можна прописати board.topics.filter(subject__contains='Hello').

Ще одна важлива річ, яку слід відзначити — те, що в коді Python нам доводиться використовувати дужки: board.topics.all(), оскільки all() — метод. Коли ми пишемо код, використовуючи Django Template Language, у файлі шаблону HTML ми не використовуємо дужки, тому пишемо просто board.topics.all.

Друга річ полягає в тому, що ми використовуємо ForeignKey:

{{ topic.starter.username }}

Просто створіть шлях через властивість, використовуючи крапки. Можна отримати доступ до будь-якої властивості моделі User. Якщо ми захочемо отримати електронну пошту користувача, то зможемо використати topic.starter.email.

Оскільки ми вже змінюємо шаблон topics.html, створімо кнопку, яка переведе нас до екрана нової теми (new topic):

templates/topics.html

{% block content %}
  <div class="mb-4">
    <a href="{% url 'new_topic' board.pk %}" class="btn btn-primary">New topic</a>
  </div>

  <table class="table">
    <!-- код обмежений для стислості -->
  </table>
{% endblock %}

Посібник по Django для початківців – Частина 3

Ми можемо включити тест, щоб впевнитись, що користувач може досягти представлення New topic із цієї сторінки:

boards/tests.py

class BoardTopicsTests(TestCase):
    # ...

    def test_board_topics_view_contains_navigation_links(self):
        board_topics_url = reverse('board_topics', kwargs={'pk': 1})
        homepage_url = reverse('home')
        new_topic_url = reverse('new_topic', kwargs={'pk': 1})

        response = self.client.get(board_topics_url)

        self.assertContains(response, 'href="{0}"'.format(homepage_url))
        self.assertContains(response, 'href="{0}"'.format(new_topic_url))

В основному, я перейменував тут старий метод test_board_topics_view_contains_link_back_to_homepage і додав додатковий assertContains. Зараз цей тест відповідає за те, щоб наше представлення містило потрібні навігаційні посилання.

Тестування представлення форми

Перш ніж програмувати приклад попередньої форми за способом Django, напишімо кілька тестів для обробки форми:

boards/tests.py

''' нові підключення модулів нижче '''
from django.contrib.auth.models import User
from .views import new_topic
from .models import Board, Topic, Post

class NewTopicTests(TestCase):
    def setUp(self):
        Board.objects.create(name='Django', description='Django board.')
        User.objects.create_user(username='john', email='john@doe.com', password='123')  # <- вставили тут цей рядок 

    # ...

    def test_csrf(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        self.assertContains(response, 'csrfmiddlewaretoken')

    def test_new_topic_valid_post_data(self):
        url = reverse('new_topic', kwargs={'pk': 1})
        data = {
            'subject': 'Test title',
            'message': 'Lorem ipsum dolor sit amet'
        }
        response = self.client.post(url, data)
        self.assertTrue(Topic.objects.exists())
        self.assertTrue(Post.objects.exists())

    def test_new_topic_invalid_post_data(self):
        '''
        Недійсні дані не повинні перенаправлятися
		Очікувана поведінка полягає в тому, щоб знову показати форму з помилками валідації
        '''
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.post(url, {})
        self.assertEquals(response.status_code, 200)

    def test_new_topic_invalid_post_data_empty_fields(self):
        '''
        Недійсні дані не повинні перенаправлятися
		Очікувана поведінка полягає в тому, щоб знову показати форму з помилками валідації
        '''
        url = reverse('new_topic', kwargs={'pk': 1})
        data = {
            'subject': '',
            'message': ''
        }
        response = self.client.post(url, data)
        self.assertEquals(response.status_code, 200)
        self.assertFalse(Topic.objects.exists())
        self.assertFalse(Post.objects.exists())

По-перше, файл tests.py вже починає ставати великим. Ми незабаром покращимо його, розбивши тести на декілька окремих файлів. Але поки що продовжимо працювати над цим.

  • setUp: включає User.objects.create_user для створення екземпляру User, який буде використовуватися в тестах
  • test_csrf: оскільки токен CSRF є основною частиною обробки запитів POST, ми повинні переконатися, що наш HTML містить токен.
  • test_new_topic_valid_post_data: надсилає дійсну комбінацію даних і перевіряє, чи створило представлення екземпляри Topic та Post.
  • test_new_topic_invalid_post_data: тут ми надсилаємо порожній словник, щоб перевірити, як поводиться застосунок.
  • test_new_topic_invalid_post_data_empty_fields: аналогічно попередньому тесту, але на цей раз ми надсилаємо деякі дані. Очікується, що застосунок буде перевіряти й відхиляти порожню тему та повідомлення.

Запустімо тести:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
........EF.....
======================================================================
ERROR: test_new_topic_invalid_post_data (boards.tests.NewTopicTests)
----------------------------------------------------------------------
Traceback (most recent call last):
...
django.utils.datastructures.MultiValueDictKeyError: "'subject'"

======================================================================
FAIL: test_new_topic_invalid_post_data_empty_fields (boards.tests.NewTopicTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/vitorfs/Development/myproject/django-beginners-guide/boards/tests.py", line 115, in test_new_topic_invalid_post_data_empty_fields
    self.assertEquals(response.status_code, 200)
AssertionError: 302 != 200

----------------------------------------------------------------------
Ran 15 tests in 0.512s

FAILED (failures=1, errors=1)
Destroying test database for alias 'default'...

У нас один не пройдений тест і одна помилка. Обидва пов'язані з неправильним вводом користувача. Замість того, щоб намагатися виправити це за допомогою поточної реалізації, спробуємо пройти ці тести, використовуючи Django Forms API.

Створення форм правильним способом

Що ж, ми пройшли довгий шлях відтоді, як почали працювати з формами. Нарешті настав час використати Forms API.

Forms API доступний у модулі django.forms. Django працює з двома типами форм: forms.Form і forms.ModelForm. Клас Form — реалізація форми загального призначення. Ми можемо використовувати його для обробки даних, які безпосередньо не пов'язані з моделлю у нашому застосунку. ModelForm є підкласом Form, і він пов'язаний з класом моделі.

Створімо новий файл із назвою forms.py всередині теки boards:

boards/forms.py

from django import forms
from .models import Topic

class NewTopicForm(forms.ModelForm):
    message = forms.CharField(widget=forms.Textarea(), max_length=4000)

    class Meta:
        model = Topic
        fields = ['subject', 'message']

Це наша перша форма. ModelForm пов'язана з моделлю Topic. subject у списку fields усередині метакласу (meta class) відноситься до поля subject у класі Topic. Тепер зверніть увагу, що ми визначаємо додаткове поле, яке називається message. Це стосується повідомлення в Post, яке ми хочемо зберегти.

Тепер нам треба переробити наш views.py:

boards/views.py

from django.contrib.auth.models import User
from django.shortcuts import render, redirect, get_object_or_404
from .forms import NewTopicForm
from .models import Board, Topic, Post

def new_topic(request, pk):
    board = get_object_or_404(Board, pk=pk)
    user = User.objects.first()  # TODO: отримати в даний час залогіненого користувача
    if request.method == 'POST':
        form = NewTopicForm(request.POST)
        if form.is_valid():
            topic = form.save(commit=False)
            topic.board = board
            topic.starter = user
            topic.save()
            post = Post.objects.create(
                message=form.cleaned_data.get('message'),
                topic=topic,
                created_by=user
            )
            return redirect('board_topics', pk=board.pk)  # TODO: перенаправити до створеної сторінки теми
    else:
        form = NewTopicForm()
    return render(request, 'new_topic.html', {'board': board, 'form': form})

Ось як ми використовуємо форми у представленні. Дозвольте мені видалити «зайвий шум», щоб ми могли зосередитись на ядрі обробки форми:

if request.method == 'POST':
    form = NewTopicForm(request.POST)
    if form.is_valid():
        topic = form.save()
        return redirect('board_topics', pk=board.pk)
else:
    form = NewTopicForm()
return render(request, 'new_topic.html', {'form': form})

Спочатку ми перевіряємо запит — він POST або GET. Якщо запит прийшов із POST, це означає, що користувач передає деякі дані на сервер. Тому ми створюємо екземпляр форми, передаючи дані POST у форму: form = NewTopicForm(request.POST).

Потім ми просимо Django перевірити дані, перевірити, чи форма є дійсною, якщо ми можемо зберегти її в базі даних: if form.is_valid():. Якщо форма дійсна, ми продовжуємо зберігати дані в базі даних, використовуючи form.save(). Метод save() повертає екземпляр моделі, збереженої в базі даних. Отже, оскільки це форма Topic, вона поверне тему, яка була створена: topic = form.save(). Після цього загальний шлях — перенаправити користувача в інше місце, щоб уникнути повторної відправки форми користувачем, натиснувши клавішу F5, а також зберегти потік застосунку.

Тепер, якщо дані були недійсними, Django додасть список помилок до форми. Після цього представлення нічого не робить та повертається в останньому стверджені: return render(request, 'new_topic.html', {'form': form}). Це означає, що ми повинні оновити new_topic.html, щоб правильно відображати помилки.

Якщо запит був GET, ми просто ініціалізуємо нову і порожню форму, використовуючи forms = NewTopicForm().

Запустімо тести й подивимося, як все працює:

python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...............
----------------------------------------------------------------------
Ran 15 tests in 0.522s

OK
Destroying test database for alias 'default'...

Ми навіть виправили останні два тести.

Django Forms API робить набагато більше, ніж просто обробка та перевірка даних. Він також генерує HTML для нас.

Оновімо шаблон new_topic.html, щоб використовувати Django Forms API в повній мірі:

templates/new_topic.html

{% extends'base.html' %}

{% block title %}Start a New Topic{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
  <li class="breadcrumb-item active">New topic</li>
{% endblock %}

{% block content %}
  <form method="post">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit" class="btn btn-success">Post</button>
  </form>
{% endblock %}

form має три варіанти рендеринга: form.as_table, form.as_ul та form.as_p. Це швидкий спосіб відрендирити всі поля форми. Як видно з назви, as_table використовує табличні теги для форматування входів, as_ul створює список HTML входів і т. д.

Подивімося, як це виглядає:

Посібник по Django для початківців – Частина 3

Що ж, наша попередня форма виглядала краще, чи не так? Ми виправимо це через мить.

Прямо зараз це може здаватися поломаним, але довіртесь мені; за цим зараз багато чого стоїть. І воно надзвичайно потужне. Наприклад, якби наша форма мала 50 полів, ми могли б відобразити всі поля просто набравши {{ form.as_p }}.

Більш того, використовуючи Forms API, Django перевірятиме дані та додаватиме повідомлення про помилки в кожне поле. Спробуємо надіслати порожню форму:

Посібник по Django для початківців – Частина 3

  • Якщо ви бачите щось на зразок цього: Посібник по Django для початківців – Частина 3, коли надсилаєте форму, то це не Django. Це ваш браузер робить передчасну валідацію. Щоб це вимкнути, додайте атрибут novalidate до тегу вашої форми: <form method="post" novalidate>. Ви можете її залишити; з цим нема ніяких проблем. Це тільки тому, що наша форма зараз дуже проста, і у нас нема великої кількості перевірки даних. Інша важлива річ, яку слід відзначити, полягає в тому, що не існує такого поняття, як «перевірка на стороні клієнта» (client-side validation). Валідація JavaScript або перевірка браузера призначена лише для зручності використання. А також зменшення кількості запитів до сервера. Перевірка даних завжди повинна виконуватися на стороні сервера, де у нас є повний контроль над даними.

Він також обробляє довідкові тексти, які можна визначити як у класі Form, так і в класі Model:

boards/forms.py

from django import forms
from .models import Topic

class NewTopicForm(forms.ModelForm):
    message = forms.CharField(
        widget=forms.Textarea(),
        max_length=4000,
        help_text='The max length of the text is 4000.'
    )

    class Meta:
        model = Topic
        fields = ['subject', 'message']

Посібник по Django для початківців – Частина 3

Ми також можемо встановити додаткові атрибути в поле форми:

boards/forms.py

from django import forms
from .models import Topic

class NewTopicForm(forms.ModelForm):
    message = forms.CharField(
        widget=forms.Textarea(
            attrs={'rows': 5, 'placeholder': 'What is on your mind?'}
        ),
        max_length=4000,
        help_text='The max length of the text is 4000.'
    )

    class Meta:
        model = Topic
        fields = ['subject', 'message']

Посібник по Django для початківців – Частина 3

Рендеринг форм Bootstrap

Добре, зробімо все знову гарним.

При роботі з Bootstrap або будь-якою іншою фронтенд бібліотекою, мені подобається використовувати пакет Django під назвою django-widget-tweaks. Він надає більше контролю над процесом рендерингу, зберігаючи значення за замовчуванням, і просто додаючи додаткові налаштування поверх нього.

Почнемо зі встановлення:

pip install django-widget-tweaks

Тепер додамо його до INSTALLED_APPS:

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'widget_tweaks',

    'boards',
]

Тепер перейдімо до використання:

templates/new_topic.html

{% extends'base.html' %}

{% load widget_tweaks %}

{% block title %}Start a New Topic{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
  <li class="breadcrumb-item active">New topic</li>
{% endblock %}

{% block content %}
  <form method="post" novalidate>
    {% csrf_token %}

    {% for field in form %}
      <div class="form-group">
        {{ field.label_tag }}

        {% render_field field class="form-control" %}

        {% if field.help_text %}
          <small class="form-text text-muted">
            {{ field.help_text }}
          </small>
        {% endif %}
      </div>
    {% endfor %}

    <button type="submit" class="btn btn-success">Post</button>
  </form>
{% endblock %}

Посібник по Django для початківців – Частина 3

Ось так! Отже, тут ми використовуємо django-widget-tweaks. Спочатку ми завантажуємо його в шаблон, використовуючи тег шаблону {% load widget_tweaks %}. Тоді використовуємо:

{% render_field field class="form-control" %}

Тег render_field не є частиною Django. Він мешкає усередині встановленого нами пакета. Щоб його використовувати, ми повинні передати екземпляр поля форми в якості першого параметру, після цього ми можемо додати довільні атрибути HTML, щоб його доповнити. Це буде корисним, оскільки тоді ми зможемо призначати класи, які основані на певних умовах.

Деякі приклади використання тегу шаблону render_field:

{% render_field form.subject class="form-control" %}
{% render_field form.message class="form-control" placeholder=form.message.label %}
{% render_field field class="form-control" placeholder="Write a message!" %}
{% render_field field style="font-size: 20px" %}

Тепер, щоб реалізувати теги валідації Bootstrap 4, ми можемо змінити шаблон new_topic.html:

new_topic.html

<form method="post" novalidate>
  {% csrf_token %}

  {% for field in form %}
    <div class="form-group">
      {{ field.label_tag }}

      {% if form.is_bound %}
        {% if field.errors %}

          {% render_field field class="form-control is-invalid" %}
          {% for error in field.errors %}
            <div class="invalid-feedback">
              {{ error }}
            </div>
          {% endfor %}

        {% else %}
          {% render_field field class="form-control is-valid" %}
        {% endif %}
      {% else %}
        {% render_field field class="form-control" %}
      {% endif %}

      {% if field.help_text %}
        <small class="form-text text-muted">
          {{ field.help_text }}
        </small>
      {% endif %}
    </div>
  {% endfor %}

  <button type="submit" class="btn btn-success">Post</button>
</form>

Результат буде таким:

Посібник по Django для початківців – Частина 3

Посібник по Django для початківців – Частина 3

Таким чином, у нас є три різних стани рендерингу:

  • Початковий стан (Initial state): форма не має даних (не пов'язана)
  • Недійсний (Invalid): ми додаємо клас CSS .is-invalid і додаємо повідомлення про помилку в елемент з класом .invalid-feedback. Поле форми та повідомлення відображаються червоним кольором.
  • Дійсний (Valid): ми додаємо клас .is-valid CSS, щоб замалювати поле форми зеленим кольором, даючи фідбек користувачеві, що це поле підходить, і можна продовжувати.

Шаблони форм багаторазового використання

Код шаблону виглядає трохи складним, вірно? Що ж, гарна новина полягає в тому, що ми можемо повторно використати цей фрагмент у проекті.

У теці templates створіть нову теку з назвою includes:

myproject/
 |-- myproject/
 |    |-- boards/
 |    |-- myproject/
 |    |-- templates/
 |    |    |-- includes/    <-- тут!
 |    |    |-- base.html
 |    |    |-- home.html
 |    |    |-- new_topic.html
 |    |    +-- topics.html
 |    +-- manage.py
 +-- venv/

Тепер всередині теки includes створіть файл з назвою form.html:

templates/includes/form.html

{% load widget_tweaks %}

{% for field in form %}
  <div class="form-group">
    {{ field.label_tag }}

    {% if form.is_bound %}
      {% if field.errors %}
        {% render_field field class="form-control is-invalid" %}
        {% for error in field.errors %}
          <div class="invalid-feedback">
            {{ error }}
          </div>
        {% endfor %}
      {% else %}
        {% render_field field class="form-control is-valid" %}
      {% endif %}
    {% else %}
      {% render_field field class="form-control" %}
    {% endif %}

    {% if field.help_text %}
      <small class="form-text text-muted">
        {{ field.help_text }}
      </small>
    {% endif %}
  </div>
{% endfor %}

Тепер змінимо наш шаблон new_topic.html:

templates/new_topic.html

{% extends 'base.html' %}

{% block title %}Start a New Topic{% endblock %}

{% block breadcrumb %}
  <li class="breadcrumb-item"><a href="{% url 'home' %}">Boards</a></li>
  <li class="breadcrumb-item"><a href="{% url 'board_topics' board.pk %}">{{ board.name }}</a></li>
  <li class="breadcrumb-item active">New topic</li>
{% endblock %}

{% block content %}
  <form method="post" novalidate>
    {% csrf_token %}
    {% include 'includes/form.html' %}
    <button type="submit" class="btn btn-success">Post</button>
  </form>
{% endblock %}

Як випливає з назви, {% include %} використовується для включення шаблонів HTML в інший шаблон. Це дуже корисний спосіб повторного використання компонентів HTML у проекті.

Для рендерингу наступної форми можемо просто використати {% include 'include /form.html' %}.

Додавання ще більшої кількості тестів

Тепер ми використовуємо Django Forms. Ми можемо додати більше тестів, щоб переконатися, що все працює гладко

boards/tests.py

# ... інші підключення модулів
from .forms import NewTopicForm

class NewTopicTests(TestCase):
    # ... інші тести

    def test_contains_form(self):  # <- новий тест
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.get(url)
        form = response.context.get('form')
        self.assertIsInstance(form, NewTopicForm)

    def test_new_topic_invalid_post_data(self):  # <- оновлення цього тесту
        '''
        Invalid post data should not redirect
        The expected behavior is to show the form again with validation errors
        '''
        url = reverse('new_topic', kwargs={'pk': 1})
        response = self.client.post(url, {})
        form = response.context.get('form')
        self.assertEquals(response.status_code, 200)
        self.assertTrue(form.errors)

Тепер ми вперше використовуємо метод assertIsInstance. В основному, ми беремо екземпляр форми в даних контексту та перевіряємо, чи це NewTopicForm. В останньому тесті ми додали self.assertTrue(form.errors), щоб переконатися, що у формі відображаються помилки, коли дані недійсні.


Висновки

У цьому уроці ми зосередилися на URL-адресах, шаблонах багаторазового використання та формах. Як завжди, ми також реалізували декілька тест кейсів. Це допомагає нам продовжувати розробку з упевненістю.

Наш тестовий файл починає ставати великим, тому в наступному уроці ми плануємо його реконструювати, щоб підтримувати зростання коду.

Ми також досягаємо точки, коли нам потрібно взаємодіяти з зареєстрованим користувачем. У наступному уроці ми дізнаємося все про автентифікацію та про те, як захистити наші представлення та ресурси.

Я сподіваюся, вам сподобалася третя частина цієї навчальної серії! Вихідний код проекту доступний на GitHub. Поточний стан проекту можна знайти за тегом випуску v0.3-lw. Наведене нижче посилання приведе вас до потрібного місця:

https://github.com/sibtc/django-beginners-guide/tree/v0.3-lw

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація