Руководство часть 5: Создание домашней страницы - Изучение веб-разработки

Руководство часть 5: Создание домашней страницы
===============================================

* Назад
* Обзор: Django
* Далее

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

| Требования: | Прочитать Введение в Django. Завершить изучение предыдущих частей руководства (включая Руководство часть 4: Django административный раздел сайта). |
| --- | --- |
| Цель: | Понимать как создавать простые url-преобразования (которые не содержат никаких данных) и отображения, как получать данные из моделей и создавать шаблоны. |

Обзор
-----

Теперь, когда мы определили наши модели и создали несколько записей в них, пришло время написать код, который будет показывать данную информацию пользователям. И первое, что нам необходимо сделать это определиться какую информацию мы бы хотели показывать на наших страницах, а затем определить соответствующие URL-адреса для получения соответствующих ресурсов. Затем нам надо создать url-преобразования, отображения (функции, или классы), а затем шаблоны страницы.

Диаграмма, представленная ниже, демонстрирует главный поток данных и элементов, которые нужно реализовать для управления HTTP запросами и ответами. Поскольку мы уже создали модель, то нам остаётся создать следующее:

* URL-преобразования для перехода по соответствующему URL-адресу (с учётом информации, передаваемой в данном адресе) к соответствующей функции отображения.
* Функции отображения для запроса соответствующих данных из моделей, создание страниц HTML для показа этих данных и их отправку в клиент пользователя (в браузер).
* Шаблоны, которые используются отображениями для рендеринга (отрисовки) данных.

![](/ru/docs/Learn/Server-side/Django/Home_page/basic-django.png)

Как вы увидите в следующем разделе, у нас будет 5 страниц, которые мы немного опишем в данной статье. Данная статья, большей частью, будет сконцентрирована на реализации всего-лишь одной, домашней страницы нашего сайта (к другим страницам мы перейдём в других частях руководства). Это должно дать вам хорошее базовое представление о работе с URL-преобразованиями (связывании), отображениями и моделями.

Определяем URL-адреса страниц
-----------------------------

По сути, так как для конечных пользователей, данная версия сайта *LocalLibrary* является read-only (только для чтения), то нам надо создать домашнюю страницу и страницы, которые будут *показывать* списки авторов и книг, а также детальную информацию о них, соответственно.

Перечислим URL-адреса, которые понадобятся для наших страниц:

* `catalog/` — Домашняя/индексная страница.
* `catalog/books/` — Список всех книг.
* `catalog/authors/` — Список всех авторов.
* `catalog/book/<id>` — Детальная информация для определённой книги со значением первичного ключа равного `<id>`. Например, `/catalog/book/3`, для `id = 3`.
* `catalog/author/<id>` — Детальная информация для определённого автора со значением первичного ключа равного `<id>`. Например, `/catalog/author/11`, для автора с `id = 11`.

Первые три URL-адреса используются для показа домашней страницы, а также списков книг и авторов. Они не кодируют никакой дополнительной информации и результат показа данных страниц будет полностью зависеть от того, что находится в базе данных и, по сути, будет все время одним и тем же (при неизменной базе данных, конечно).

Последние два URL-адреса применяются для показа детальной информации об определённой книге, или авторе — в себе они содержат соответствующее значение идентификатора (показан как `<id>`, выше). URL-преобразование получает данную информацию и передаёт её в отображение, которое применяет её для запроса к базе данных. Для кодирования и применения данной информации в вашем URL-адресе нам понадобится только одно url-преобразование, соответствующее отображение и шаблон страницы для показа любой книги (или автора).

**Примечание:** Django позволяет вам конструировать ваши URL-адреса любым, удобным для вас, способом — вы можете закодировать информацию в теле URL-адреса, как показано выше, или использовать URL-адрес типа `GET` (например, `/book/?id=6`). Независимо от ваших предпочтений, URL-адреса должны быть понятными, логичными и читабельными (посмотрите совет W3C здесь).

Документация Django рекомендует кодировать информацию в теле URL-адреса, на практике это приводит к лучшей структуре сайта.

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

Создание главной страницы сайта
-------------------------------

Первой страницей, которую мы создадим, будет главная страница сайта (`catalog/`). Она будет небольшой статической HTML-страницей, которая будет показывать вычисленные "количества" различных записей из базы данных. Для того, чтобы проделать данную работу мы вначале создадим URL-преобразование, затем отображение и шаблон.

**Примечание:** Лучше уделить больше внимания на данный раздел, поскольку информация, представленная здесь, применяется для создания всех страниц сайта.

### URL-преобразование

Когда мы создавали скелет сайта мы обновили **locallibrary/urls.py** так что всякий раз, когда начинается URL-адрес наш catalog/ получен и URLConf catalog.urls подключён для обработки оставшейся части строки.
```

urlpatterns += [
    path('catalog/', include('catalog.urls')),
]
```

Примечание: всякий раз, когда Django сталкивается c `django.urls.include()` он отбрасывает часть совпавшего URL , и отправляет оставшуюся строку в включённый URLconf для дальнейшей обработки.

Внутри нашего каталога приложения откройте **urls.py** и поместите в него текст, отмеченный жирным, ниже.

```python
urlpatterns = [
    path('', views.index, name='index'),
]
```

Эта функция `path()` определяет URL-паттерн (в данном случае это пустая строка: `'' -` мы поговорим чуть более подробно о них далее в данном руководстве) и функцию отображения, которая будет вызвана, если введённый адрес будет соответствует данному паттерну (`views.index` — это функция с именем `index()` в **views.py**).

Данная функция `path()`, кроме того, определяет параметр `name`, который уникально определяет *это* частное URL-преобразование. Вы можете использовать данное имя для "обратного" ("reverse") преобразования — то есть, для динамического создания URL-адреса, указывающего на ресурс, на которое указывает данное преобразование. Например, теперь, когда у нас имеется данное символическое имя, мы можем ссылаться на нашу домашнюю страницу при помощи создания следующей ссылки внутри какого-либо шаблона:

```html
<a href="{% url 'index' %}">Home</a>.
```

**Примечание:** Мы могли бы, конечно, жёстко указать прямую ссылку (то есть, `<a href="/catalog/">Home</a>`), но тогда, если мы изменим адрес нашей домашней страницы (например на `/catalog/index`), то данные ссылки перестанут корректно работать. Применение "обратного" url-преобразования более гибкий и эффективный подход!

### Отображения (на основе функций)

Отображение является функцией, которая обрабатывает HTTP-запрос, получает данные из базы данных (при необходимости), которые применяются для генерации страницы HTML. Затем функция отображения возвращает сгенерированную страницу пользователю в виде HTTP-ответа. В нашем случае, индексная функция демонстрирует этот процесс — она получает информацию о количестве записей `Book`, `BookInstance`, доступности `BookInstance`, а также записи `Author` из базы данных, затем передаёт эти записи в шаблон страницы, генерирует страницу и передаёт её пользователю (клиенту пользователя, например браузеру).

Откройте **catalog/views.py** и отметьте для себя, что данный файл уже импортирует функцию render() - функцию, которая генерирует HTML-файлы при помощи шаблонов страниц и соответствующих данных.

```python
from django.shortcuts import render

# Создайте ваше отображение здесь
```

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

```python
from .models import Book, Author, BookInstance, Genre

def index(request):
    """
 Функция отображения для домашней страницы сайта.
 """
    # Генерация "количеств" некоторых главных объектов
    num_books=Book.objects.all().count()
    num_instances=BookInstance.objects.all().count()
    # Доступные книги (статус = 'a')
    num_instances_available=BookInstance.objects.filter(status__exact='a').count()
    num_authors=Author.objects.count()  # Метод 'all()' применён по умолчанию.

    # Отрисовка HTML-шаблона index.html с данными внутри
    # переменной контекста context
    return render(
        request,
        'index.html',
        context={'num\_books':num_books,'num\_instances':num_instances,'num\_instances\_available':num_instances_available,'num\_authors':num_authors},
    )
```

Первая часть функции отображения получает количество записей при помощи вызова функции `objects.all()` у атрибута `objects`, доступного для всех классов моделей. Похожим образом мы получаем список объектов `BookInstance`, которые имеют статус 'a' (Доступно). Вы можете найти дополнительную информацию о работе с моделями в предыдущей части руководства (Руководство часть 3: Применение моделей > Поиск записей).

В конце функции `index` вызывается функция `render()`, которая, в качестве ответа, создаёт и возвращает страницу HTML (эта функция "оборачивает" вызовы нескольких функций, тем самым существенно упрощая процесс разработки). В качестве параметров ей передаются объект `request` (типа `HttpRequest`), шаблон HTML-страницы с метками (`placeholders`), которые будут замещены данными, а также переменной `context` (словарь Python, который содержит данные, которые и будут замещать метки в шаблоне).

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

### Шаблон

Шаблон это текстовый файл, который определяет структуру и расположение данных в файле, кроме того, в нем размещают специальные метки (placeholders), которые используются для показа реального содержимого, то есть данных. По умолчанию Django ищет файлы шаблонов в директории с именем '**templates**' внутри вашего приложения. Например, внутри индексной функции отображения, которую мы только что создали, вызов `render()` будет пытаться найти файл **/locallibrary/catalog/templates/*index.html*** и в случае неудачи сгенерирует ошибку о том, что файл не найден. Вы можете увидеть данную ошибку, если вы сохраните предыдущие изменения, затем перейдёте в браузер и наберёте в адресной строке `127.0.0.1:8000`. В результате, в окно браузера будет выведено сообщение об ошибке "TemplateDoesNotExist at /catalog/" и некоторая другая информация.

**Примечание:** На самом деле, в зависимости от настроек проекта, Django просматривает несколько мест в поисках шаблона (поиск в директории приложения осуществляется по умолчанию!). Вы можете найти больше информации о шаблонах и форматах, которые они поддерживают, перейдя по ссылке Шаблоны (Django docs).

#### Расширение шаблонов

Шаблон главной страницы нашего сайта должен соответствовать стандарту разметки HTML для разделов `head` и `body`, кроме того иметь разделы для навигации (на другие страницы, которые мы создадим позже) и показа некоторого вводного текста. Большая часть данной структуры будет одинаковой для всех страниц нашего сайта. Таким образом, чтобы избежать копирования одной и той же информации, язык создания шаблонов Django позволяет вам объявить базовый шаблон, а затем расширить его, замещая только те части, которые являются специфическими для каждой страницы.

Например, базовый шаблон **base\_generic.html** может выглядеть как показано ниже. Как вы видите, этот файл содержит некоторую "общую" структуру HTML, разделы для заголовка, панель навигации и содержимое, отмеченное тэгами шаблона `block` и `endblock` (показано жирным). Данные блоки могут быть пустыми, или иметь содержимое, которое будет использоваться "по умолчанию" всеми страницами-наследниками.

**Примечание:** *Тэги* шаблона подобны функциям, которые могут применяться для создания циклов по спискам, выполнять условные операции и так далее. Кроме тэгов, язык шаблона позволяет использовать переменные (которые передаются в шаблон из отображения), а также *шаблонные фильтры*, которые переформатируют переменные (например, переводят строку в нижний регистр).

```django
<!doctype html>
<html lang="en">
<head>
  {% block title %}<title>Local Library</title>{% endblock %}
</head>

<body>
  {% block sidebar %}<!-- insert default navigation text for every page -->{% endblock %}
  {% block content %}<!-- default content text (typically empty) -->{% endblock %}
</body>
</html>
```

Когда мы определяем шаблон для конкретного отображения, то в первую очередь мы объявляем базовый шаблон (при помощи тэга `extends` — смотрите код в следующем фрагменте). Если имеются блоки в базовом шаблоне, которые мы хотим заместить, тогда в нашем текущем шаблоне мы объявляем `block`/`endblock` и указываем соответствующее имя блока.

Например фрагмент кода, показанный ниже, демонстрирует применение тэга `extends` и переопределяет блок с именем `content`. Окончательный код HTML будет содержать все структуры базового файла шаблона (включая содержимое по умолчанию, которое мы указали в блоке `title`) и код блока `content`, который мы разместили в текущем файле шаблона.

```django
{% extends "base\_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>
  <p>
    Welcome to <em>LocalLibrary</em>, a very basic Django website developed as a
    tutorial example on the Mozilla Developer Network.
  </p>
{% endblock %}
```

#### Базовый шаблон сайта LocalLibrary

Базовый шаблон, который мы планируем использовать для сайта *LocalLibrary*, представлен ниже. Как вы видите, данный фрагмент содержит HTML код и объявляет следующие блоки `title`, `sidebar` и `content`. Мы добавили заголовок по умолчанию (который, возможно, мы захотим изменить), а также боковую панель навигации, содержащей ссылки на списки всех книг и авторов (панель навигации, мы, вероятно, не будем менять/замещать, но, тем не менее, добавив этот блок, мы оставим для себя такую возможность).

**Примечание:** Во фрагменте мы используем два дополнительных шаблонных тега: `url` и `load static`. Они будут описаны в следующих разделах.

Создайте новый файл — **/locallibrary/catalog/templates/*base\_generic.html*** — и добавьте в него следующее содержимое:

```django
<!doctype html>
<html lang="en">
  <head>
    {% block title %}<title>Local Library</title>{% endblock %}
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link
 rel="stylesheet"
 href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

    <!-- Добавление дополнительного статического CSS файла -->
    {% load static %}
    <link rel="stylesheet" href="{% static 'css/styles.css' %}" />
  </head>

  <body>
    <div class="container-fluid">
      <div class="row">
        <div class="col-sm-2">
          {% block sidebar %}
            <ul class="sidebar-nav">
              <li><a href="{% url 'index' %}">Home</a></li>
              <li><a href="">All books</a></li>
              <li><a href="">All authors</a></li>
            </ul>
          {% endblock %}
        </div>
        <div class="col-sm-10 ">{% block content %}{% endblock %}</div>
      </div>
    </div>
  </body>
</html>
```

Шаблон использует (и включает в себя) JavaScript и CSS от Bootstrap для лучшего размещения элементов и формирования внешнего вида HTML страницы. Применение Bootstrap, или любого другого фреймворка для клиентской части сайта, является довольно продуктивным способом повышения привлекательности страницы, в том числе, это учитывает возможность запроса и показа сайта с устройств, с различными разрешениями экрана, а кроме того, это позволяет нам повысить уровень взаимодействия с пользователем — мы направим большую часть своих усилий на серверную часть нашего сайта!

Базовый шаблон ссылается на локальный файл css (**styles.css**), который предоставляет дополнительные стили. Создайте **/locallibrary/catalog/static/css/styles.css** и добавьте в него следующее содержимое:

```css
.sidebar-nav {
  margin-top: 20px;
  padding: 0;
  list-style: none;
}
```

#### Индексный шаблон (шаблон главной страницы сайта)

Создайте файл HTML **/locallibrary/catalog/templates/*index.html*** и скопируйте в него код, указанный ниже. Как вы наверное заметили, в первой строке мы расширяем наш базовый шаблон, а затем замещаем содержимое блока `content`, базового шаблона, новым содержимым текущего шаблона.

```django
{% extends "base\_generic.html" %}

{% block content %}
<h1>Local Library Home</h1>

  <p>Welcome to <em>LocalLibrary</em>, a very basic Django website developed as a tutorial example on the Mozilla Developer Network.</p>

<h2>Dynamic content</h2>

  <p>The library has the following record counts:</p>
  <ul>
    <li><strong>Books:</strong> {{ num\_books }}</li>
    <li><strong>Copies:</strong> {{ num\_instances }}</li>
    <li><strong>Copies available:</strong> {{ num\_instances\_available }}</li>
    <li><strong>Authors:</strong> {{ num\_authors }}</li>
  </ul>

{% endblock %}
```

В данном фрагменте, в разделе *Динамическое содержимое,* мы объявили метки (*шаблонные переменные*) для информации, которую мы получаем из соответствующего отображения. Данные переменные объявляются при помощи "двойных фигурных скобок" (в предыдущем фрагменте выделено жирным).

**Примечание:** Переменные шаблона заключаются в двойные фигурные скобки (`{{ num_books }}`) , а тэги шаблона (функции шаблона), помещаются в одинарные фигурные скобки со знаками процента (`{% extends "base_generic.html" %}`).

Важно отметить, что данные переменные имеют имена, соответствующие именам передаваемых *ключей* из словаря переменной `context`, которая, в свою очередь, передаётся из отображения, во время вызова функции `render()` (смотри ниже). При отрисовке шаблона, вместо этих ключей будут подставлены, соответствующие им, *значения*.

```python
return render(
    request,
    'index.html',
     context={'num\_books':num_books,'num\_instances':num_instances,'num\_instances\_available':num_instances_available,'num\_authors':num_authors},
)
```

#### Ссылка на статические файлы их шаблонов

Любой ваш проект с большой вероятностью будет использовать статические ресурсы, включая JavaScript, CSS и изображения. В связи с тем, что расположение этих файлов может быть неизвестно (или может измениться), Django позволяет вам в шаблоне указать относительное расположение данных файлов при помощи глобального значения `STATIC_URL` (по умолчанию, значение параметра `STATIC_URL` установлено в '`/static/`', но вы можете выбрать любое другое значение, указав, например, сетевой ресурс, или что-то ещё).

Внутри шаблона вы вызываете функцию (тэг) `load`, которая загружает статическую библиотеку "static" (как показано ниже). После того как статическая библиотека загружена, вы можете использовать тэг шаблона `static`, который указывает относительный путь URL к интересующему вас файлу.

```django
<!-- Добавляем дополнительный статический CSS-файл -->
{% load static %}
<link rel="stylesheet" href="{% static 'css/styles.css' %}" />
```

Тем же способом вы можете загрузить нужное изображение. Например:

```django
{% load static %}
<img
 src="{% static 'catalog/images/local\_library\_model\_uml.png' %}"
 alt="My image"
 style="width:555px;height:540px;" />
```

**Примечание:** Фрагменты выше указывают пути расположения файлов, но Django не использует их по умолчанию. В процессе разработки сервер использует значения, указанные в глобальном файле URL-преобразований (**/locallibrary/locallibrary/urls.py**), который мы создали в части создание скелета сайта. В дальнейшем, в продакшене, вам нужно будет уточнить параметры расположения статических файлов. Мы вернёмся к этому позже.

Для получения более подробной информации о работе со статическими файлами обратитесь к документации по ссылке Управление статическими файлами (Django docs).

#### Построение URL-адресов

Базовый шаблон, указанный выше, вводит тэг `url`.

```python
<li><a href="{% url 'index' %}">Home</a></li>
```

Данный тэг с именем `url()`, ищет в файле **urls.py** связанное значение переменной, указанной в качестве её параметра `'index'`, а затем возвращает URL, который вы можете использовать для ссылки на соответствующие ресурсы.

Как теперь все это выглядит?
----------------------------

На данный момент мы должны были сделать всё что необходимо, для того, чтобы показать главную страницу нашего сайта. Запустите сервер (`python3 manage.py runserver`) и введите в ваш браузер адрес `http://127.0.0.1:8000/`. Если все настроено как надо, то ваш сайт должен выглядеть как показано на следующей картинке.

![Index page for LocalLibrary website](/ru/docs/Learn/Server-side/Django/Home_page/index_page_ok.png)

**Примечание:** На данном этапе вы не сможете воспользоваться ссылками на страницы **All books** и **All authors**, потому что url-адреса, отображения и шаблоны для данных страниц не созданы (мы просто объявили метки для соответствующих ссылок в базовом шаблоне `base_generic.html`).

Проверьте себя
--------------

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

1. В главном файле шаблона (*base\_generic.html*) есть блок `title`. Переопределите этот блок в индексном шаблоне (*index.html*) и задайте новый заголовок для этой страницы.
2. Модифицируйте функцию отображения таким образом, чтобы получать из базы данных количество жанров и количество книг, которые содержат в своих заголовках какое-либо слово (без учёта регистра), а затем передайте эти значения в шаблон.
Итог
----

Мы создали домашнюю страницу для нашего сайта — HTML страница, которая показывает количество некоторых записей из базы данных и содержит ссылки на другие "все-ещё-будут-созданы" страницы. Кроме того, мы изучили большое количество базовой информации об url-преобразованиях, отображениях, запросах к базе данных, используя наши модели, передачу информации из отображений в шаблоны, кроме того, создание и расширение шаблонов.

В следующей части, при помощи наших новых знаний, мы создадим ещё четыре страницы.

Смотрите также
--------------

* Написание вашего первого приложения Django, часть 3: Отображения и Шаблоны (Django docs)
* URL-диспетчер (Django docs)
* Функции отображения (DJango docs)
* Шаблоны (Django docs)
* Управление статическими файлами (Django docs)
* Удобные (встроенные) функции Django (Django docs)
* Назад
* Обзор: Django
* Далее


Django учебник Часть 3: Использование моделей - Изучение веб-разработки

Django учебник Часть 3: Использование моделей
=============================================

* Назад
* Обзор: Django
* Далее

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

| Предпосылки | Django Tutorial Part 2: Creating a skeleton website. |
| --- | --- |
| Задача: | Научиться проектировать и создавать свои собственные модели, выбирая подходящие поля. |

Обзор
-----

Веб-приложения Django получают доступ и управляют данными через объекты Python, называемые моделями. Модели определяют структуру хранимых данных, включая типы полей и, возможно, их максимальный размер, значения по умолчанию, параметры списка выбора, текст справки для документации, текст меток для форм и т. д. Определение модели не зависит от основной базы данных - вы можете выбрать один из нескольких компонентов вашей настройки проекта. После того, как вы выбрали какую базу данных хотите использовать, вам не нужно напрямую работать с ней - вы просто пишете свою структуру модели и код, а Django делает всю грязную работу, связанную с базой данных за вас.

В этом учебнике показано, как определить и получить доступ к моделям на примере LocalLibrary website.

Проектирование моделей LocalLibrary
-----------------------------------

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

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

При проектировании ваших моделей имеет смысл иметь отдельные модели для каждого «объекта» (группа связанной информации). В этом случае очевидными объектами являются книги, экземпляры книг и авторы.

Вы также можете использовать модели для представления параметров списка выбора (например, как выпадающий список вариантов), вместо жёсткого кодирования выбора на самом веб-сайте - это рекомендуется, когда все варианты неизвестны заранее или могут измениться. Очевидные кандидаты на модели в этом случае включают жанр книги (например, «Научная фантастика», «Французская поэзия» и т. д.) И язык (английский, французский, японский).

Как только мы определились с нашими моделями и полями, нам нужно подумать об отношениях. Django позволяет вам определять отношения, как один к одному (`OneToOneField`), один ко многим (`ForeignKey`) и многие ко многим (`ManyToManyField`).

Диаграмма ассоциации UML, приведённая ниже показывает модели, которые мы определили в этом случае (в виде блоков). Как и выше, мы создали модели для книги (общие сведения о книге), экземпляр книги (статус конкретных физических копий книги, доступных в системе) и автора.Мы также решили создать модель для жанра, чтобы можно было создавать / выбирать значения через интерфейс администратора. Мы решили не иметь модель для BookInstance: status - мы жёстко закодировали значения (LOAN\_STATUS), потому что мы не ожидаем их изменения. В каждом из полей вы можете увидеть имя модели, имена и типы полей, а также методы и их типы возврата.

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

![LocalLibrary Model UML - v3](/ru/docs/Learn/Server-side/Django/Models/local_library_model_uml.png)

**Примечание:** Примечание. В следующем разделе приведён базовый пример, поясняющий, как модели определяются и используются. Когда вы его прочитаете, подумайте, как мы построим каждую из моделей на диаграмме выше.

Модель для начинающих
---------------------

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

### Определение модели

Модели обычно определяются в приложении **models.py**. Они реализуются как подклассы `django.db.models.Model`, и могут включать поля, методы и метаданные. В приведённом ниже фрагменте кода показана «типичная» модель, названная `MyModelName`:

```python
from django.db import models
from django.urls import reverse

class MyModelName(models.Model):
    """Типичный класс модели, производный от класса Model."""

    # Поля
    my_field_name = models.CharField(max_length=20, help_text='Введите описание поля')
    # …

    # Метаданные
    class Meta:
        ordering = ['-my\_field\_name']

    # Methods
    def get\_absolute\_url(self):
        """Возвращает URL-адрес для доступа к определенному экземпляру MyModelName."""
        return reverse('model-detail-view', args=[str(self.id)])

    def \_\_str\_\_(self):
        """Строка для представления объекта MyModelName (например, в административной панели и т.д.)."""
        return self.my_field_name
```

В следующих разделах мы подробно рассмотрим каждый элемент внутри модели:

#### Поля

Модель может иметь произвольное количество полей любого типа - каждый представляет столбец данных, который мы хотим сохранить в одной из наших таблиц базы данных. Каждая запись (строка) базы данных будет состоять из одного значения каждого поля. Давайте рассмотрим приведённый выше пример:

```python
my_field_name = models.CharField(max_length=20, help_text="Введите описание поля")
```

Наш вышеприведённый пример имеет одно поле, называемое my\_`field_name`, типа `models.CharField` — что означает, что это поле будет содержать строки буквенно-цифровых символов. Типы полей назначаются с использованием определённых классов, которые определяют тип записи, которая используется для хранения данных в базе данных, а также критерии проверки, которые должны использоваться, когда значения получены из формы HTML (то есть, что составляет действительное значение). Типы полей также могут принимать аргументы, которые дополнительно определяют, как поле хранится или может использоваться. В этом случае мы даём нашему полю два аргумента:

* `max_length=20` — Указывает, что максимальная длина значения в этом поле составляет 20 символов.
* `help_text="Введите описание поля"` — предоставляет текстовую метку для отображения, чтобы помочь пользователям узнать, какое значение необходимо предоставить, когда это значение должно быть введено пользователем через HTML-форму.

Имя поля используется для обращения к нему в запросах и шаблонах. В полях также есть метка, которая задаётся как аргумент (`verbose_name`), либо выводится путём заглавной буквы первой буквы имени переменной поля и замены любых символов подчёркивания пробелом (например, `my_field_name` будет иметь метку по умолчанию *My field name*).

Порядок, в котором объявляются поля, будет влиять на их порядок по умолчанию, если модель отображается в форме (например, на сайте администратора), хотя это может быть переопределено.

##### Общие аргументы поля

Следующие общие аргументы могут использоваться при объявлении многих / разных типов полей:

* help\_text: Предоставляет текстовую метку для HTML-форм (например, на сайте администратора), как описано выше.
* verbose\_name: Удобочитаемое имя для поля, используемого в поле метки. Если не указано, Django выведет по умолчанию подробное название от имени поля.
* default: Значение по умолчанию для поля. Это может быть значение или вызываемый объект, и в этом случае объект будет вызываться каждый раз, когда создаётся новая запись.
* null: Если True, Django будет хранить пустые значения как NULL в базе данных для полей, где это уместно (CharField вместо этого сохранит пустую строку). По умолчанию используется значение False.
* blank: Если True, поле может быть пустым в ваших формах. По умолчанию используется значение False, что означает, что проверка формы Django заставит вас ввести значение. Это часто используется с null = True, потому что если вы хотите разрешить пустые значения, вы также хотите, чтобы база данных могла представлять их соответствующим образом.
* choices: Группа вариантов для этого поля. Если это предусмотрено, по умолчанию соответствующий виджет формы будет полем выбора с этими вариантами вместо стандартного текстового поля.
* primary\_key: Если True, задаёт текущее поле в качестве первичного ключа для модели (первичный ключ - это специальный столбец базы данных, предназначенный для однозначной идентификации всех разных записей таблицы). Если в качестве первичного ключа не указано поле, Django автоматически добавит для этой цели поле.

Есть много других вариантов - вы можете просмотреть full list of field options here.

##### Общие типы полей

Следующие общие аргументы могут использоваться при объявлении многих / разных типов полей:

* CharField Используется для определения строк фиксированной длины от короткой до средней. Вы должны указать max\_length для хранения данных.
* TextField используется для больших строк произвольной длины. Вы можете указать `max_length` для поля, но это используется только тогда, когда поле отображается в формах (оно не применяется на уровне базы данных).
* IntegerField это поле для хранения значений (целого числа) и для проверки введённых значений в виде целых чисел в формах.
* DateField и DateTimeField используются для хранения / представления дат и информации о дате / времени (как Python datetime.date и datetime.datetime, соответственно). Эти поля могут дополнительно объявлять (взаимоисключающие) параметры `auto_now=True` (для установки поля на текущую дату каждый раз, когда модель сохраняется), auto\_now\_add (только для установки даты, когда модель была впервые создана) и по умолчанию (чтобы установить дату по умолчанию, которую пользователь может переустановить).
* EmailField используется для хранения и проверки адресов электронной почты.
* FileField и ImageField используются для загрузки файлов и изображений соответственно ( `ImageField` просто добавляет дополнительную проверку, что загруженный файл является изображением). Они имеют параметры для определения того, как и где хранятся загруженные файлы.
* AutoField - это особый тип IntegerField, который автоматически увеличивается. Первичный ключ этого типа автоматически добавляется в вашу модель, если вы явно не укажете его.
* ForeignKey Используется для указания отношения «один ко многим» к другой модели базы данных (например, автомобиль имеет одного производителя, но производитель может делать много автомобилей). «Одна» сторона отношения - это модель, содержащая ключ.
* ManyToManyField используется для определения отношения «многие ко многим» (например, книга может иметь несколько жанров, и каждый жанр может содержать несколько книг). В нашем приложении для библиотек мы будем использовать их аналогично ForeignKeys, но их можно использовать более сложными способами для описания отношений между группами. Они имеют параметр on\_delete, чтобы определить, что происходит, когда связанная запись удаляется (например, значение `models.SET_NULL` просто установило бы значение NULL)

Существует много других типов полей, включая поля для разных типов чисел (большие целые числа, малые целые числа, дробные), логические значения, URL-адреса, slugs, уникальные идентификаторы и другие «связанные с временем» сведения (продолжительность, время и т. д.). Вы можете просмотреть full list here.

#### Метаданные

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

```python
class Meta:
    ordering = ["-my\_field\_name"]
    ...
```

Одной из наиболее полезных функций этих метаданных является управление сортировка записей, возвращаемых при запросе типа модели. Вы можете сделать это, указав соответствия названия полей для сортировки, как показано выше. Порядок будет зависеть от типа поля (поля символов отсортированы в алфавитном порядке, а поля даты отсортированы в хронологическом порядке). Как показано выше, вы можете префикс имени поля минус-символом (-), чтобы изменить порядок сортировки.

Например, если мы решили сортировать книги по умолчанию:

```python
ordering = ["title", "-pubdate"]
```

Книги будут отсортированы по алфавиту по названию, от A-Z, а затем по дате публикации внутри каждого названия, от самого нового до самого старого.

Другим распространённым атрибутом является verbose\_name, подробное имя для класса в единственной и множественной форме:

```python
verbose_name = "BetterName"
```

Другие полезные атрибуты позволяют создавать и применять новые «разрешения доступа» для модели (разрешения по умолчанию применяются автоматически), разрешить упорядочение на основе другого поля или объявить, что класс является «абстрактным» (базовый класс, для которого вы не можете создавать записи, и вместо этого будет создан для создания других моделей). Многие другие параметры метаданных управляют тем, какая база данных должна использоваться для модели и как хранятся данные (это действительно полезно, если вам нужно сопоставить модель с существующей базой данных). Полный список опций метаданных доступен здесь: Model metadata options (Django документация).

#### Методы

Модель также может иметь методы. Минимально в каждой модели вы должны определить стандартный метод класса для Python \_\_str \_\_ (), чтобы вернуть удобочитаемую строку для каждого объекта. Эта строка используется для представления отдельных записей на сайте администрирования (и в любом другом месте, где вам нужно обратиться к экземпляру модели). Часто это возвращает поле названия или имени из модели.

```python
def \_\_str\_\_(self):
    return self.field_name
```

Другим распространённым методом включения в модели Django является get\_absolute\_url (), который возвращает URL-адрес для отображения отдельных записей модели на веб-сайте (если вы определяете этот метод, тогда Django автоматически добавит кнопку «Просмотр на сайте» на экранах редактирования записей модели на сайте администратора). Типичный шаблон для get\_absolute\_url () показан ниже.

```python
def get\_absolute\_url(self):
    """
 Returns the url to access a particular instance of the model.
 """
    return reverse('model-detail-view', args=[str(self.id)])
```

**Примечание:** Примечание. Предполагается, что вы будете использовать URL-адреса, например / myapplication / mymodelname / 2, для отображения отдельных записей для вашей модели (где «2» - это идентификатор для определённой записи), вам нужно будет создать URL-карту, чтобы передать ответ и идентификатор «Образцовое представление модели» (которое будет выполнять работу, необходимую для отображения записи). Вышеуказанная функция reverse () может «перевернуть» ваш URL-адрес (в приведённом выше примере с именем «model-detail-view»), чтобы создать URL-адрес правильного формата.

Конечно, для выполнения этой работы вам всё равно придётся писать сопоставление URL-адрес, просмотр и шаблон!

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

### Управление моделью

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

#### Создание и изменение записей

Чтобы создать запись, вы можете определить экземпляр модели, а затем вызвать метод save ().

```python
# Create a new record using the model's constructor.
a_record = MyModelName(my_field_name="Instance #1")

# Save the object into the database.
a_record.save()
```

**Примечание:** Примечание. Если вы не указали какое-либо поле в качестве primary\_key, новая запись будет выдаваться автоматически, с идентификатором имени поля. Вы можете запросить это поле после сохранения указанной выше записи, и оно будет иметь значение 1.

Вы можете получить доступ к полям в этой новой записи с использованием синтаксиса точек и изменить значения. Вы должны вызвать save (), чтобы сохранить изменённые значения в базе данных.

```python
# Access model field values using Python attributes.
print(a_record.id) #should return 1 for the first record.
print(a_record.my_field_name) # should print 'Instance #1'

# Change record by modifying the fields, then calling save().
a_record.my_field_name="New Instance Name"
a_record.save()
```

#### Поиск записей

Вы можете искать записи, соответствующие определённым критериям, используя атрибут объектов модели (предоставляемый базовым классом).

**Примечание:** Примечание. Объяснение того, как искать записи, используя «абстрактную» модель и имена полей, может быть немного запутанным. В приведённом ниже обсуждении мы будем ссылаться на модель книги с полями названия и жанра, где жанр также является моделью с единственным именем в поле.

Мы можем получить все записи для модели как объект QuerySet, используя `objects.all()`. QuerySet - это итерируемый объект, означающий, что он содержит несколько объектов, которые мы можем перебирать / прокручивать.

```python
all_books = Book.objects.all()
```

Метод `filter()` Django позволяет отфильтровать возвращаемый `QuerySet` для соответствия указанному текстовому или числовому полю по конкретным критериям. Например, чтобы отфильтровать книги, содержащие слово «wild» («дикие») в заголовке, а затем подсчитать их, мы могли бы сделать следующее.

```python
wild_books = Book.objects.filter(title__contains='wild')
number_wild_books = Book.objects.filter(title__contains='wild').count()
```

Соответствующие поля и тип соответствия определяются в имени параметра фильтра, используя формат: `field_name__match_type` (обратите внимание на двойное подчёркивание между заголовком выше). Выше мы фильтруем заголовок с учётом регистра. Есть много других типов совпадений, которые вы можете сделать: `icontains` (без учёта регистра), `iexact` (точное совпадение без учёта регистра), `exact` (точное совпадение с учётом регистра ) и `in`, `gt` (больше), `startswith` и т. д смотреть полный список(Django Docs, [EN]).

В некоторых случаях вам нужно будет фильтровать поле, которое определяет отношение «один ко многим» к другой модели (например, `ForeignKey`). В этом случае вы можете «индексировать» поля в связанной модели с дополнительными двойными подчёркиваниями. Так, например, чтобы фильтровать книги с определённым жанровым рисунком, вам нужно будет указывать имя в поле жанра, как показано ниже:

```python
books_containing_genre = Book.objects.filter(genre__name__icontains='fiction')  # Will match on: Fiction, Science fiction, non-fiction etc.
```

**Примечание:** Вы можете использовать символы подчёркивания (\_\_) для навигации по многим уровням отношений (ForeignKey / ManyToManyField) по своему усмотрению. Например, книга, имеющая разные типы, определяемая с использованием дополнительной связи «обложка», может иметь имя параметра: type\_\_cover\_\_name\_\_exact = 'hard'.

Существует гораздо больше возможностей для запросов, включая обратные поиски от связанных моделей, цепочки фильтров, возврат меньшего набора значений и т. д. Для получения дополнительной информации см. Making queries (Django Docs, [EN]).

Определение моделей LocalLibrary
--------------------------------

В этом разделе мы начнём определять модели для библиотеки. Откройте *models.py (в / locallibrary / catalog /)*. Шаблон в верхней части страницы импортирует модуль моделей, который содержит базовый класс модели `models.Model`, от которого наследуются наши модели.

```python
from django.db import models

# Create your models here.
```

### Модель жанра

Скопируйте приведённый ниже код модели `Genre` и вставьте его в нижнюю часть вашего файла `models.py`. Эта модель используется для хранения информации о категории книг - например, будь то художественная или документальная, роман или военно-историческая и т. д. Как уже упоминалось выше, мы создали жанр как модель, а не как свободный текст или список выбора, чтобы возможные значения могли управляться через базу данных, а не были закодированными.

```python
class Genre(models.Model):
    """
 Model representing a book genre (e.g. Science Fiction, Non Fiction).
 """
    name = models.CharField(max_length=200, help_text="Enter a book genre (e.g. Science Fiction, French Poetry etc.)")

    def \_\_str\_\_(self):
        """
 String for representing the Model object (in Admin site etc.)
 """
        return self.name
```

Модель имеет один `CharField` field (имя), которое используется для описания жанра (оно ограничено 200 символами и имеет некоторый `help_text`. В конце модели мы объявляем метод `__str__()`, который просто возвращает имя жанра, определённого конкретной записью. Verbose name не был определён, поэтому поле будет называться `Name` в формах.

### Модель книги

Скопируйте модель книги ниже и снова вставьте её в нижнюю часть файла. Модель книги представляет всю информацию о доступной книге в общем смысле, но не конкретный физический «экземпляр» или «копию» для временного использования. Модель использует CharField для представления названия книги и isbn (обратите внимание, как isbn указывает свой ярлык как «ISBN», используя первый неименованный параметр, поскольку в противном случае ярлык по умолчанию был бы «Isbn»). Модель использует TextField для summary, потому что этот текст, возможно, должен быть очень длинным.

```python
from django.urls import reverse #Used to generate URLs by reversing the URL patterns

class Book(models.Model):
    """
 Model representing a book (but not a specific copy of a book).
 """
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    # Foreign Key used because book can only have one author, but authors can have multiple books
    # Author as a string rather than object because it hasn't been declared yet in the file.
    summary = models.TextField(max_length=1000, help_text="Enter a brief description of the book")
    isbn = models.CharField('ISBN',max_length=13, help_text='13 Character <a href="https://www.isbn-international.org/content/what-isbn">ISBN number</a>')
    genre = models.ManyToManyField(Genre, help_text="Select a genre for this book")
    # ManyToManyField used because genre can contain many books. Books can cover many genres.
    # Genre class has already been defined so we can specify the object above.

    def \_\_str\_\_(self):
        """
 String for representing the Model object.
 """
        return self.title

    def get\_absolute\_url(self):
        """
 Returns the url to access a particular book instance.
 """
        return reverse('book-detail', args=[str(self.id)])
```

Жанр представляет из себя ManyToManyField, так что книга может иметь несколько жанров, а жанр может иметь много книг. Автор объявляется через ForeignKey, поэтому в каждой книге будет только один автор, но у автора может быть много книг (на практике книга может иметь несколько авторов, но не в такой реализации!)

В обоих типах полей соответствующий класс модели объявляется как первый неименованный параметр, используя либо класс модели, либо строку, содержащую имя соответствующей модели. Вы должны использовать имя модели как строку, если связанный класс ещё не был определён в этом файле до того, как он будет указан! Другими параметрами, представляющими интерес для поля автора, являются `null=True`, которое позволяет базе данных хранить значение `Null` , если автор не выбран, и on\_delete = models. `SET_NULL` установит значение автора в Null, если связанная с автором запись будет удалена.

Модель также определяет \_\_str \_\_ (), используя поле заголовка книги для представления книги. Окончательный метод get\_absolute\_url () возвращает URL-адрес, который можно использовать для доступа к подробной записи для этой модели (для этого нам нужно будет определить сопоставление URL-адресов, в котором содержится подробная информация о книге, и определить связанное представление и шаблон ).

### Модель BookInstance

Затем скопируйте модель BookInstance (показано ниже) под другие модели. BookInstance представляет собой определённую копию книги, которую кто-то может брать взаймы, и включает информацию о том, доступна ли копия или в какой день она ожидается, «отпечаток» или сведения о версии, а также уникальный идентификатор книги в библиотеке. Теперь некоторые из полей и методов будут знакомы. Модель использует

* ForeignKey для идентификации связанной книги (в каждой книге может быть много копий, но в копии может быть только одна книга).
* CharField, для представления данных (конкретного выпуска) о книге.

```python
import uuid # Required for unique book instances

class BookInstance(models.Model):
    """
 Model representing a specific copy of a book (i.e. that can be borrowed from the library).
 """
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, help_text="Unique ID for this particular book across whole library")
    book = models.ForeignKey('Book', on_delete=models.SET_NULL, null=True)
    imprint = models.CharField(max_length=200)
    due_back = models.DateField(null=True, blank=True)

    LOAN_STATUS = (
        ('m', 'Maintenance'),
        ('o', 'On loan'),
        ('a', 'Available'),
        ('r', 'Reserved'),
    )

    status = models.CharField(max_length=1, choices=LOAN_STATUS, blank=True, default='m', help_text='Book availability')

    class Meta:
        ordering = ["due\_back"]

    def \_\_str\_\_(self):
        """
 String for representing the Model object
 """
        return '%s (%s)' % (self.id,self.book.title)
```

Мы дополнительно объявляем несколько новых типов полей:

* `UUIDField` используется для поля id, чтобы установить его как primary\_key для этой модели. Этот тип поля выделяет глобальное уникальное значение для каждого экземпляра (по одному для каждой книги, которую вы можете найти в библиотеке).
* `DateField` используется для данных due\_back (при которых ожидается, что книга появится после заимствования или обслуживания). Это значение может быть blank или null (необходимо, когда книга доступна). Метаданные модели (Class Meta) используют это поле для упорядочивания записей, когда они возвращаются в запросе.
* status - это CharField, который определяет список choice/selection. Как вы можете видеть, мы определяем кортеж, содержащий кортежи пар ключ-значение и передаём его аргументу выбора. Значение в key/value паре - это отображаемое значение, которое пользователь может выбрать, а ключи - это значения, которые фактически сохраняются, если выбрана опция. Мы также установили значение по умолчанию «m» (техническое обслуживание), поскольку книги изначально будут созданы недоступными до того, как они будут храниться на полках.

Модель \_\_str \_\_ () представляет объект BookInstance, используя комбинацию его уникального идентификатора и связанного с ним заголовка книги.

**Примечание:** Примечание. Немного Python:

* Значение, возвращаемое \_\_str \_\_ (), является форматированной строкой. В строке мы используем % S для объявления 'placeholders'. После строки укажем %, а затем кортеж, содержащий значения, которые будут вставлены в заполнители. Если у вас просто один заполнитель, вы можете опустить кортеж - например, 'Моё значение:% S' % переменная.
 Обратите также внимание на то, что, хотя этот подход совершенно применим, но он более не является предпочтительным. Начиная с Python 3, вы должны использовать метод format, например. '{0} ({1})'.format (self.id, self.book.title). Вы можете узнать больше об этом здесь.

### Модель автора

Скопируйте модель автора (показано ниже) под существующим кодом в models.py.

Теперь все поля/методы должны быть знакомы. Модель определяет автора как имя, фамилию, дату рождения и (необязательную) дату смерти. Он указывает, что по умолчанию \_\_str \_\_ () возвращает имя в фамилии, порядковый номер первого имени. Метод get\_absolute\_url () отменяет сопоставление URL-адреса автора с целью получения URL-адреса для отображения отдельного автора.

```python
class Author(models.Model):
    """
 Model representing an author.
 """
    first_name = models.CharField(max_length=100)
    last_name = models.CharField(max_length=100)
    date_of_birth = models.DateField(null=True, blank=True)
    date_of_death = models.DateField('Died', null=True, blank=True)

    def get\_absolute\_url(self):
        """
 Returns the url to access a particular author instance.
 """
        return reverse('author-detail', args=[str(self.id)])

    def \_\_str\_\_(self):
        """
 String for representing the Model object.
 """
        return '%s, %s' % (self.last_name, self.first_name)
```

Повторно выполнить миграцию базы данных
---------------------------------------

Теперь все ваши модели созданы. Теперь переустановите миграцию базы данных, чтобы добавить их в свою базу данных.
```

python3 manage.py makemigrations
python3 manage.py migrate
```

Языковая модель - вызов
-----------------------

Представьте себе, что местный благотворитель жертвует ряд новых книг, написанных на другом языке (скажем, фарси). Задача состоит в том, чтобы определить, как они будут лучше всего представлены на нашем веб-сайте библиотеки, а затем добавить их в модели.

Некоторые вещи, которые следует учитывать:

* Должен ли «язык» ассоциироваться с Book, BookInstance или каким-либо другим объектом?
* Должны ли быть представлены разные языки с использованием модели, свободного текстового поля или жёстко запрограммированного списка выбора?

После того, как вы решили, добавьте поле. Вы можете увидеть наше решение на Github here.

Резюме
------

В этой статье мы узнали, как определять модели, а затем использовать эту информацию в разработке и внедрении соответствующих моделей для сайта LocalLibrary.

На этом этапе мы отвлечёмся от создания сайта и проверим *Django Administration site*. Этот сайт позволит нам добавить некоторые данные в библиотеку, которые мы можем отобразить с помощью наших (ещё не созданных) представлений и шаблонов.

Смотрите также
--------------

* Writing your first Django app, part 2 (Django Docs)
* Making queries (Django Docs)
* QuerySet API Reference (Django Docs)
* Назад
* Обзор: Django
* Далее


Безопасность веб-приложения Django - Изучение веб-разработки

Безопасность веб-приложения Django
==================================

* Назад
* Обзор: Django
* Далее

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

| Требования: | Прочитайте тему Веб безопасность. Завершите изучение предыдущих частей руководства до Руководство часть 9: Работа с формами включительно. |
| --- | --- |
| Цель: | Понять, что нужно делать (или наоборот не делать), для обеспечения безопасности вашего веб-приложения. |

Обзор
-----

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

**Предупреждение:** **Важно:** Наиболее важный урок, который вы должны усвоить, состоит в том - что никогда не стоит доверять переданным пользователем данным. Они включают в себя GET параметры в URL, тело POST запроса, HTTP заголовки, cookies, загруженные пользователем данные и т.д. Всегда проверяйте и обрабатывайте все входные данные. Всегда готовьтесь к худшему.

Хорошей новостью для всех разработчиков, использующих Django, является то, что большинство известных атак обрабатывается фреймворком! Статья Безопасность в Django (Django docs) описывает методы обеспечения безопасности Django и стратегии защиты веб-приложения разработанного на данном фреймворке.

Распространённые угрозы/методы защиты
-------------------------------------

Мы не будем дублировать документацию Django и в данной статье продемонстрируем некоторые основные методы обеспечения безопасности в контексте разрабатываемого в данном руководстве приложения LocalLibrary.

### Межсайтовый скриптинг (XSS)

XSS это термин, применяющийся для описания класса атак, позволяющего атакующему, через веб-сайт внедрить скрипты, которые будут выполнены на устройстве зашедшего на страницу пользователя. Часто это происходит через сохранение вредоносного кода в базе данных, откуда данный код будет возвращён и выполнен для запросившего некие данные пользователя (типичный пример - сохранение тега <script> с вредоносным кодом в комментарии, который может увидеть другой пользователь). Другой вектор атаки - в том чтобы сгенерировать определённую ссылку, при клике на которую пользователь запустит выполнение некоего замаскированного кода JavaScript в своём браузере.

Система шаблонов Django защищает от большинства XSS-атак, экранируя определённые символы, считающиеся "опасными" в HTML. Мы можем продемонстрировать это, попытавшись внедрить произвольный JavaScript-код в наше приложение LocalLibrary через форму добавления автора, созданную в Руководство часть 9: Работа с формами.

1. Запустите веб-сайт, используя сервер разработки (`python3 manage.py runserver`).
2. Откройте сайт в вашем браузере и войдите под аккаунтом супер-пользователя.
3. Перейдите на страницу добавления автора (она должна быть доступна по URL: `http://127.0.0.1:8000/catalog/author/create/`).
4. Введите данные об имени и фамилии, датах рождения и смерти автора. Затем добавьте следующую строку в поле фамилии:
 `<script>alert('Test alert');</script>`.

 ![Author Form XSS test](/ru/docs/Learn/Server-side/Django/web_application_security/author_create_form_alert_xss.png)

**Примечание:** Это безобидный скрипт, который, если будет выполнен, отобразит окно с сообщением "Test alert" в вашем браузере. Если данное окно отображается при открытии страницы с созданной подобным образом записью - значит сайт уязвим перед атаками XSS.
5. Нажмите **Submit** для сохранения записи.
6. После сохранения автора - он должен быть отображён, как показано ниже. Так как сработала защита от XSS - команда `alert()` не будет запущена. Вместо этого скрипт будет отображаться как обычный текст.
 ![Author detail view XSS test](/ru/docs/Learn/Server-side/Django/web_application_security/author_detail_alert_xss.png)

Если вы посмотрите исходный HTML код, вы увидите, что "опасные" символы - например такие как скобки тегов - были заменены на их безопасные эквивалентные html сущности (к примеру `>` на `>`)

```html
<h1>
  Author: Boon<script>alert('Test alert');</script>, David
  (Boonie)
</h1>
```

Использование шаблонов Django защищает вас от большинства XSS-атак. Однако существует возможность отключения данной защиты, при котором экранирование не будет автоматически применятся ко всем полям, которые не должны будут заполнятся пользователем(к примеру, поле `help_text` обычно заполняется не пользователем, поэтому Django не будет экранировать его значение).

Так же XSS-атаки могут быть осуществлены через другие ненадёжные источники данных, такие как cookies, сторонние сервисы или загруженные файлы (и прочие источники, данные которых не были специально обработаны перед отображением на странице). Если вы отображаете данные из этих источников, вы должны добавить ваш собственный обработчик для фильтрации данных.

### Межсайтовая подделка запроса (CSRF)

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

**Примечание:** Очевидно, что наш хакер делает это не ради денег! Более амбициозные хакеры могут использовать описываемый подход для выполнения более опасных задач (например, переводы денег пользователей на их личные счета и т.д).

Для того, чтобы сделать это, хакер может создать HTML файл, подобный продемонстрированному ниже, который будет содержать форму создания автора (похожую на ту, что мы разрабатывали в предыдущих частях руководства), которая будет отправлена как только данный файл будет загружен в браузер. Хакер отправит данный файл всем Библиотекарям и будет ждать пока кто-либо из них откроет файл (он содержит только безобидную информацию, честно!). Если файл будет открыт любым залогиненным пользователем, с правами Библиотекаря - тогда форма будет отправлена от его имени и создаст нового пользователя.

```html
<html>
  <body onload="document.EvilForm.submit()">
    <form
 action="http://127.0.0.1:8000/catalog/author/create/"
 method="post"
 name="EvilForm">
      <table>
        <tr>
          <th><label for="id\_first\_name">First name:</label></th>
          <td>
            <input
 id="id\_first\_name"
 maxlength="100"
 name="first\_name"
 type="text"
 value="Mad"
 required />
          </td>
        </tr>
        <tr>
          <th><label for="id\_last\_name">Last name:</label></th>
          <td>
            <input
 id="id\_last\_name"
 maxlength="100"
 name="last\_name"
 type="text"
 value="Man"
 required />
          </td>
        </tr>
        <tr>
          <th><label for="id\_date\_of\_birth">Date of birth:</label></th>
          <td>
            <input id="id\_date\_of\_birth" name="date\_of\_birth" type="text" />
          </td>
        </tr>
        <tr>
          <th><label for="id\_date\_of\_death">Died:</label></th>
          <td>
            <input
 id="id\_date\_of\_death"
 name="date\_of\_death"
 type="text"
 value="12/10/2016" />
          </td>
        </tr>
      </table>
      <input type="submit" value="Submit" />
    </form>
  </body>
</html>
```

Запустите веб-сервер разработки и войдите в аккаунт супер-пользователя. Скопируйте приведённый выше текст в файл и затем откройте его в браузере. Вы должны получить CSRF ошибку, потому что у Django есть защита от атак данного вида!

Механизм защиты заключается в том, что вы добавляете тег шаблона `{% csrf_token %}` в вашу форму. Этот токен будет отображён в вашем HTML как показано ниже, со значением, уникальным для каждого запрашивающего форму пользователя.

```html
<input
 type="hidden"
 name="csrfmiddlewaretoken"
 value="0QRWHnYVg776y2l66mcvZqp8alrv4lb8S8lZ4ZJUWGZFA5VHrVfL2mpH29YZ39PW" />
```

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

Для продолжения использования данного вида атак, хакер теперь должен найти и добавить верный CSRF токен для каждого выбранного целью пользователя. Это означает, что хакер теперь не может использовать массовые рассылки одного вредоносного файла всем Библиотекарям, так как для каждого из них CSRF токен будет уникальным.

Защита Django от CSRF атак по умолчанию включена. Вам всегда следует использовать тег`{% csrf_token %}` в ваших формах и использовать `POST` для запросов, которые могут изменить или добавить данные в вашу базу данных.

### Другие атаки

Django так же предоставляет защиту от других видов атак ( демонстрация большинства из которых была бы сложна новичкам для понимания и не слишком полезна ):

Защита от SQL инъекции

Уязвимость SQL инъекции позволяет атакующему выполнить произвольный SQL код в базе данных и получить доступ к данным (прочитать, отредактировать и изменить) независимо от текущих прав доступа пользователя. В большинстве случаев вы будете получать доступ к данным базы данных, используя сущности queryset/model Django. Используя их для генерации SQL запросов, вы получите корректно сформированный и экранированный запрос для выбранной базы данных. Если вам необходимо писать "сырые" запросы, вам так же нужно будет продумать защиту от инъекций.

Защита от Кликджекинга

В данном виде атак атакующий перехватывает ввод на видимом слое страницы и перенаправляет их на скрытый слой под ним. Этот метод может быть использован к примеру для отображения официального сайта банка, с перехватом данных для входа в невидимом `<iframe>` (en-US), который контролирует атакующий. Django содержит защиту от кликджекинга в виде `промежуточного програмного обеспечения (middleware) X-Frame-Options,` который поддерживается браузерами и может запретить отображение страницы внутри `<iframe>` (en-US).

SSL/HTTPS

SSL/HTTPS может быть использован на веб-сервере для шифрования всего трафика между сервером и пользователем, включая данные входа, которые иначе будут отправляться как обычный текст (который сможет прочитать любой перехвативший запрос человек). Использование HTTPS высоко рекомендовано. Если используется HTTPS, Django позволяет использовать следующие методы защиты:

* `SECURE_PROXY_SSL_HEADER` может быть использовано для проверки что всегда используется безопасное подключение, даже если данные поступают из прокси, использующего протокол отличный от HTTP.
* `SECURE_SSL_REDIRECT` используется для перенаправления всех запросов с HTTP на HTTPS.
* Используйте HTTP Strict Transport Security (HSTS). Этот HTTP заголовок информирует браузер о том, что все последующие запросы должны всегда использовать HTTPS. Совместно с перенаправлением HTTP запросов на HTTPS, эта опция позволяет обеспечить использование HTTPS в каждом запросе. HSTS может так же быть настроен опциями `SECURE_HSTS_SECONDS` и `SECURE_HSTS_INCLUDE_SUBDOMAINS` или на веб-сервере.
* Используйте 'безопасные' cookies выставив `SESSION_COOKIE_SECURE` и `CSRF_COOKIE_SECURE` в `True`. Это позволит обеспечить пересылку данных cookies только через протокол HTTPS.

Валидация заголовка Host

Используйте `ALLOWED_HOSTS` чтобы принимать только запросы от доверенных хостов.

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

Подводим итоги
--------------

Django имеет методы обеспечения защиты от распространённых видов атак, включая XSS и CSRF атаки. В данной статье мы продемонстрировали, как различные виды атак обрабатываются Django на примере нашего приложения *LocalLibrary*. Мы так же кратко рассмотрели другие виды уязвимостей и методы защиты от них.

Это было очень краткое погружение в вопрос веб-безопасности. Мы крайне рекомендуем вам прочитать Безопасность в Django для более глубокого понимания.

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

Смотрите также
--------------

* Безопасность в Django (Django docs)
* Веб безопасность (MDN)
* Безопасность вашего сайта (en-US) (MDN)
* Назад
* Обзор: Django
* Далее
In this module
--------------

* Django introduction
* Setting up a Django development environment
* Django Tutorial: The Local Library website
* Django Tutorial Part 2: Creating a skeleton website
* Django Tutorial Part 3: Using models
* Django Tutorial Part 4: Django admin site
* Django Tutorial Part 5: Creating our home page
* Django Tutorial Part 6: Generic list and detail views
* Django Tutorial Part 7: Sessions framework
* Django Tutorial Part 8: User authentication and permissions
* Django Tutorial Part 9: Working with forms
* Django Tutorial Part 10: Testing a Django web application
* Django Tutorial Part 11: Deploying Django to production
* Django web application security
* DIY Django mini blog


Задание: DIY Django мини блог - Изучение веб-разработки

Задание: DIY Django мини блог
=============================

* Назад
* Обзор: Django

В этом задании вы будете оценивать знания Django, которые вы приобрели в Django Web Framework (Python), чтобы создать очень простой блог.

| Предпосылки: | Перед этим заданием, вы должны были проработать все статьи этого модуля. |
| --- | --- |
| Задача: | Проверить понимание основ Django, включая конфигурации URL, модели, представления, формы и шаблоны. |

Краткое описание проекта
------------------------

Страницы, которые должны отображаться, их URL-адреса и другие требования, перечислены ниже:

| Page | URL | Requirements |
| --- | --- | --- |
| Home page | `/` and `/blog/` | Страница индекса, описывающая сайт. |
| List of all blog posts | `/blog/blogs/` | Список всех сообщений блога:* Доступно для всех пользователей из боковой панели.
* Список отсортирован по дате публикации (от самого нового до самого
 старого).
* Список разбит на группы по 5 статьям.
* Элементы списка отображают название блога, дату публикации и автора.
* Названия сообщений блога связаны с страницами подробных сведений о
 блоге.
* Blogger (имена авторов) связаны с страницами подробных сведений о
 блоге.
 |
| Blog author (blogger) detail page | `/blog/blogger/*<author-id>*` | Информация для указанного автора (по id) и список постов:* Доступен для всех пользователей по ссылкам на автора в сообщениях в
 блогах и т. Д.
* Содержит некоторые биографические данные в blogger/author.
* Список отсортирован по дате добавления (от новых к старым).
* Не разбит на страницы.
* Элементы списка отображают только имя сообщения в блоге и дату
 публикации.
* Названия блога связаны со страницей блога.
 |
| Blog post detail page | `/blog/*<blog-id>*` | Сведения о блоге.* Доступно для всех пользователей из списков блога.
* Страница содержит сообщение в блоге: имя, автор, дата публикации и
 содержание.
* Комментарии к сообщению в блоге должны отображаться внизу.
* Комментарии должны быть отсортированы по порядку: от старых до самых
 последних.
* Содержит ссылку для добавления комментариев на конец для
 зарегистрированных пользователей (см. Страницу формы комментариев)
* В блогах и комментариях должен отображаться только обычный текст.
 Нет необходимости поддерживать какую-либо разметку HTML (например,
 ссылки, изображения, полужирный / курсив и т. Д.).
 |
| List of all bloggers | `/blog/bloggers/` | Список блогеров в системе:* Доступный для всех пользователей с боковой панели сайта
* Имя блогера связано с блогом автора страницы.
 |
| Comment form page | `/blog/*<blog-id>*/create` | Создать комментарий для публикации в блоге:* Доступно только зарегистрированным пользователям (только) из ссылки
 внизу страницы с подробными сведениями блога.
* Отображает форму с описанием для ввода комментариев (дата публикации
 и блог недоступны для редактирования).
* После того, как комментарий будет опубликован, страница будет
 перенаправлена на связанную страницу блога.
* Пользователи не могут редактировать или удалять свои сообщения.
* Вышедшие пользователи будут перенаправлены на страницу входа в
 систему, чтобы добавить комментарии. После входа в систему они будут
 перенаправлены на страницу блога, которую они хотели бы
 прокомментировать.
* Страницы комментариев должны содержать имя / ссылку на комментарий
 блога.
 |
| User authentication pages | `/accounts/*<standard urls>*` |
 Стандартные страницы аутентификации Django для входа, выхода и
 установки пароля:
 * Вход / выход должен быть доступен через ссылки боковой панели.
 |
| Admin site | `/admin/*<standard urls>*` |
 Админ-сайт должен быть включён, чтобы разрешить создание /
 редактирование / удаление сообщений в блогах, авторов блога и
 комментариев блога (это механизм для создания блогеров в блогах):
 * В админ панеле должен отображаться список комментариев в строке
 (внизу каждого сообщения в блоге).
* Имена комментариев в админке создаются усеканием описания
 комментария до 75 знаков
* Другие типы записей могут использовать базовую регистрацию.
 |

Кроме того, вы должны написать некоторые базовые тесты для проверки:

* Все поля модели имеют правильную метку и длину.
* Все модели имеют ожидаемое имя объекта (например, `__str__()` выдаёт ожидаемое значение).
* Модели имеют ожидаемый URL для отдельных записей в блогах и комментариях (например, `get_absolute_url()` возвращает ожидаемый URL-адрес).
* Страница BlogListView (страница на всех блогах) доступна в ожидаемом месте (например, /blog/blogs)
* Страница BlogListView (страница на всех блогах) доступна на ожидаемом именованном URL-адресе (например, 'blogs')
* Страница BlogListView (страница на всех блогах) использует ожидаемый шаблон (например, по умолчанию)
* BlogListView разбивает записи на 5 (по крайней мере, на первой странице)

**Примечание:** Конечно, есть много других тестов, которые вы можете запустить. Используйте на своё усмотрение, но мы ожидаем, что вы сделаете хотя бы тесты выше.

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

Скриншоты
---------

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

### Список всех сообщений в блоге

Это отображает список всех сообщений в блоге (доступны из ссылки "All blogs" на боковой панели). Что нужно отметить:

* На боковой панели также списки вошедшего в систему пользователя.
* Индивидуальные блоги и блогеры доступны в виде ссылок на странице.
* Разбивка включена (в группах по 5)
* Показ от новых к старым.

![List of all blogs](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_allblogs.png)

### Список всех блогеров

Это ссылки на всех блогеров в "All bloggers" по ссылке, которая на боковой панели. В этом случае мы можем увидеть на боковой панели, что ни один пользователь не вошёл в систему.

![List of all bloggers](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_allbloggers.png)

### Подробная страница блога

Это показывает подробную страницу для конкретного блога.

![Blog detail with add comment link](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_detail_add_comment.png)

Обратите внимание, что комментарии имеют дату *и* время, и расположены в порядке от самых старых до новейших (противоположно порядку ведения блога). В конце у нас есть ссылка для доступа к форме, чтобы добавить новый комментарий. Если пользователь не вошёл в систему, мы бы увидели предложение войти в систему.

![Comment link when not logged in](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blog_detail_not_logged_in.png)

### Добавить форму комментария

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

![Add comment form](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_comment_form.png)

### Об авторе

Здесь отображается информация о блогере вместе со списком его блогов.

![Blogger detail page](/ru/docs/Learn/Server-side/Django/django_assessment_blog/diyblog_blogger_detail.png)

Завершающие шаги
----------------

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

1. Создайте скелет проекта и веб-приложение для сайта (как описано в Django Tutorial Part 2: Creating a skeleton website). Вы можете использовать «diyblog» для имени проекта и «blog» для имени приложения.
2. Создавайте модели для записей в блогах, комментариев и любых других необходимых объектов.
    * Каждый комментарий будет иметь только один блог, но блог может иметь много комментариев.
    * Посты в блоге и комментарии должны быть отсортированы по дате поста.
    * Не каждый пользователь обязательно будет автором блога, хотя любой пользователь может быть комментатором.
    * Блог автора также должен включать информацию о себе.
3. Запустите миграцию для новых моделей и создайте суперпользователя.
4. Используйте админ панель, чтобы создать какой-нибудь пример блога и комментарии в блогах.
5. Создайте представления, шаблоны, и URL-конфигурации для публикации блога и списка страниц блогера.
6. Создайте представления, шаблоны, и URL-конфигурации для публикации блога и подробных страниц блогера.
7. Создайте страницу с формой для добавления новых комментариев (не забудьте сделать это доступным только для зарегистрированных пользователей!)
Советы и подсказки
------------------

Этот проект очень похож на LocalLibrary учебник. Вы сможете настроить скелет, поведение входа пользователя / выхода из системы, поддержку статических файлов, представлений, URL-адресов, форм, базовых шаблонов и конфигурации админ-панели, используя почти все те же подходы.

Некоторые общие рекомендации:

1. Индексная страница (index page) может быть реализована в качестве основной функции представления и шаблона (как и для locallibrary).
2. Просмотр списка публикаций блога и блогеров, а также подробное представление для сообщений в блоге можно создать с помощью generic list and detail views.
3. Список постов в блоге конкретного автора может быть создан с помощью общего списка Blog list view и фильтрация для объекта блога, соответствующего указанному автору.
    * Вам придётся реализовать `get_queryset(self)` для фильтрации (как и в нашем классе библиотеки `LoanedBooksAllListView`) и получить информацию об авторе из URL-адреса.
    * Вам также необходимо передать имя автора на страницу в контексте. Чтобы сделать это в представлении на основе классов, вам необходимо реализовать `get_context_data()` (обсуждается ниже).
4. Форма *добавления комментариев* может быть создана с использованием функционального представления (и связанной модели и формы) или с использованием общего `CreateView`. Если вы используете `CreateView` (рекомендуется):
    * Вам также нужно будет передать имя блога на страницу комментариев в контексте (реализовать `get_context_data()` как обсуждается ниже).
    * Форма должна отображать только комментарий «описание» для записи пользователя (дата и связанная с ними запись в блоге не должны редактироваться). Поскольку они не будут в форме, ваш код должен будет установить автора комментария в `form_valid()` функцию, поэтому он может быть сохранён в модели (as described here — Django docs). В этой же функции мы устанавливаем связанный блог. Возможная реализация показана ниже (`pk` это идентификатор блога, переданный из URL / URL конфигурации ).
     python
    ```python
        def form\_valid(self, form):
            """
     Add author and associated blog to form data before setting it as valid (so it is saved to model)
     """
            #Add logged-in user as author of comment
            form.instance.author = self.request.user
            #Associate comment with blog based on passed id
            form.instance.blog=get_object_or_404(Blog, pk = self.kwargs['pk'])
            # Call super-class form validation behaviour
            return super(BlogCommentCreate, self).form_valid(form)

    ```
    * Для успешного перенаправления после проверки формы вам нужно будет указать URL-адрес; это должен быть оригинальный блог. Для этого вам нужно будет переопределить `get_success_url()` и «обратный» URL-адрес для исходного блога. Вы можете получить требуемый ID блога, используя `self.kwargs` атрибут, как показано в методе `form_valid()` выше.

Мы кратко говорили о передаче контекста шаблону в представлении на основе классов в теме Django Tutorial Part 6: Generic list and detail views. Для этого вам нужно переопределить `get_context_data()` (сначала получить существующий контекст, обновить его любыми дополнительными переменными, которые вы хотите передать шаблону, а затем вернуть обновлённый контекст). Например, фрагмент кода ниже показывает, как вы можете добавить объект blogger в контекст на основе его `BlogAuthor` id.

```python
class SomeView(generic.ListView):
    ...

    def get\_context\_data(self, \*\*kwargs):
        # Call the base implementation first to get a context
        context = super(SomeView, self).get_context_data(\*\*kwargs)
        # Get the blogger object from the "pk" URL parameter and add it to the context
        context['blogger'] = get_object_or_404(BlogAuthor, pk = self.kwargs['pk'])
        return context
```

Аттестация
----------

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

После того, как вы завершили этот модуль, вы также закончили весь контент MDN для изучения базового веб-сайта на сервере Django! Надеемся, вам понравится этот модуль и вы почувствуете, что у вас есть хорошее понимание основ!

* Назад
* Обзор: Django