Spec-Zone.ru › Django 1.11

Создание вашего первого приложения Django, часть 2

Этот учебник продолжается с того места, где закончился Учебник 1. Мы настроим базу данных, создадим вашу первую модель и получим краткое введение в автоматически сгенерированную администраторскую панель Django.

Настройка базы данных

Теперь откройте mysite/settings.py. Это обычный Python-модуль с переменными уровня модуля, представляющими настройки Django.

По умолчанию конфигурация использует SQLite. Если вы новичок в базах данных или просто хотите попробовать Django, это самый простой выбор. SQLite включен в Python, поэтому вам не нужно устанавливать ничего дополнительного для поддержки вашей базы данных. Однако, при начале реального проекта, вы можете захотеть использовать более масштабируемую базу данных, например PostgreSQL, чтобы избежать проблем с переключением баз данных в будущем.

Если вы хотите использовать другую базу данных, установите соответствующие привязки к базе данных и измените следующие ключи в элементе DATABASES 'default' для соответствия настройкам подключения к вашей базе данных:

  • ENGINE – Либо 'django.db.backends.sqlite3', 'django.db.backends.postgresql', 'django.db.backends.mysql', или 'django.db.backends.oracle'. Другие бэкэнды также доступны.
  • NAME – Имя вашей базы данных. Если вы используете SQLite, база данных будет файлом на вашем компьютере; в этом случае NAME должен быть полным абсолютным путем, включая имя файла, этого файла. Значение по умолчанию os.path.join(BASE_DIR, 'db.sqlite3'), сохранит файл в каталоге вашего проекта.

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

Для баз данных, отличных от SQLite

Если вы используете базу данных, отличную от SQLite, убедитесь, что вы создали базу данных к этому моменту. Сделайте это с помощью «CREATE DATABASE database_name;» в интерактивном приглашении вашей базы данных.

Также убедитесь, что у пользователя базы данных, указанного в mysite/settings.py, есть права «создавать базу данных». Это позволяет автоматически создавать тестовую базу данных, которая потребуется в более позднем учебнике.

Если вы используете SQLite, вам не нужно создавать ничего предварительно — файл базы данных будет создан автоматически при необходимости.

Во время редактирования mysite/settings.py, установите TIME_ZONE на вашу временную зону.

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

По умолчанию INSTALLED_APPS содержит следующие приложения, все из которых поставляются с Django:

  • django.contrib.admin – Администраторская панель. Вы скоро её будете использовать.
  • django.contrib.auth – Система аутентификации.
  • django.contrib.contenttypes – Фреймворк для типов контента.
  • django.contrib.sessions – Фреймворк для управления сессиями.
  • django.contrib.messages – Фреймворк для сообщений.
  • django.contrib.staticfiles – Фреймворк для управления статическими файлами.

Эти приложения включены по умолчанию для удобства в распространенном случае.

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

$ python manage.py migrate

Команда migrate анализирует настройку INSTALLED_APPS и создаёт все необходимые таблицы базы данных в соответствии с настройками базы данных в вашем файле mysite/settings.py и миграциями базы данных, поставляемыми с приложением (мы рассмотрим их позже). Вы увидите сообщение для каждой применяемой миграции. Если вас это интересует, запустите командную строку для вашей базы данных и введите \dt (PostgreSQL), SHOW TABLES; (MySQL), .schema (SQLite) или SELECT TABLE_NAME FROM USER_TABLES; (Oracle), чтобы отобразить таблицы, созданные Django.

Для минималистов

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

Создание моделей

Теперь мы определим ваши модели — по существу, вашу структуру базы данных с дополнительными метаданными.

Философия

Модель является единственным, определяющим источником истины о ваших данных. Она содержит основные поля и поведение данных, которые вы храните. Django следует принципу DRY. Цель состоит в том, чтобы определить вашу модель данных в одном месте и автоматически выводить вещи из неё.

Это включает в себя миграции — в отличие, например, от Ruby On Rails, миграции полностью выводятся из файла моделей и по существу являются лишь историей, которую Django может пройти, чтобы обновить схему вашей базы данных для соответствия вашим текущим моделям.

В нашем простом приложении опросов мы создадим две модели: Question и Choice. Question имеет вопрос и дату публикации. Choice имеет два поля: текст варианта ответа и количество голосов. Каждый Choice связан с Question.

Эти понятия представлены простыми Python-классами. Отредактируйте файл polls/models.py, чтобы он выглядел так:

from django.db import models


class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')


class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

Код довольно простой. Каждая модель представлена классом, который наследуется от django.db.models.Model. Каждая модель имеет ряд переменных класса, каждая из которых представляет поле базы данных в модели.

Каждое поле представлено экземпляром класса Field — например, CharField для символьных полей и DateTimeField для дат и времени. Это сообщает Django, какой тип данных содержит каждое поле.

Имя каждого экземпляра Field (например, question_text или pub_date) — имя поля в удобном для машины формате. Вы будете использовать это значение в своём Python-коде, и ваша база данных будет использовать его как имя столбца.

Вы можете использовать необязательный первый позиционный аргумент к Field для обозначения удобочитаемого имени. Это используется в некоторых рефлексивных частях Django, и это также является документацией. Если это поле не указано, Django будет использовать имя в удобном для машины формате. В этом примере мы определили удобочитаемое имя только для Question.pub_date. Для всех остальных полей в этой модели машиночитаемого имени достаточно как удобочитаемого.

Некоторые классы Field имеют обязательные аргументы. CharField, например, требует, чтобы вы указали max_length. Это используется не только в схеме базы данных, но и в валидации, как мы увидим вскоре.

END_OF_DOCUMENT_MARKER

Поле Field также может иметь различные необязательные аргументы; в этом случае мы установили значение default в votes равное 0.

Наконец, обратите внимание на определение связи с использованием ForeignKey. Это указывает Django, что каждое Choice связано с одним Question. Django поддерживает все распространённые базовые отношения: многие-к-одному, многие-ко-многим и один-к-одному.

Активация моделей

Этот небольшой фрагмент кода модели предоставляет Django много информации. С его помощью Django может:

  • Создать схему базы данных (CREATE TABLE инструкции) для этого приложения.
  • Создать API доступа к базе данных Python для доступа к объектам Question и Choice.

Но сначала нужно сообщить нашему проекту, что приложение polls установлено.

Философия

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

Чтобы включить приложение в наш проект, нам нужно добавить ссылку на его конфигурационный класс в настройку INSTALLED_APPS. Класс PollsConfig находится в файле polls/apps.py, поэтому его путь с точками — 'polls.apps.PollsConfig'. Откройте файл mysite/settings.py и добавьте этот путь с точками в настройку INSTALLED_APPS. Он будет выглядеть так:

INSTALLED_APPS = [
    'polls.apps.PollsConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

Теперь Django знает, как включить приложение polls. Давайте выполним ещё одну команду:

$ python manage.py makemigrations polls

Вы должны увидеть что-то подобное:

Migrations for 'polls':
  polls/migrations/0001_initial.py:
    - Create model Choice
    - Create model Question
    - Add field question to choice

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

Миграции — способ, которым Django сохраняет изменения в ваших моделях (и, следовательно, в схеме вашей базы данных) — это просто файлы на диске. Вы можете прочитать миграцию для своей новой модели, если хотите; это файл polls/migrations/0001_initial.py. Не волнуйтесь, от вас не ожидается чтение каждого файла, создаваемого Django, но они предназначены для редактирования человеком, если вы хотите вручную настроить то, как Django вносит изменения.

Существует команда, которая выполнит миграции за вас и автоматически управляет схемой вашей базы данных — это migrate, и мы рассмотрим её чуть позже — но сначала давайте посмотрим, какой SQL будет выполнена эта миграция. Команда sqlmigrate принимает имена миграций и возвращает их SQL:

$ python manage.py sqlmigrate polls 0001

Вы должны увидеть что-то подобное (мы переформатировали его для лучшей читаемости):

BEGIN;
--
-- Create model Choice
--
CREATE TABLE "polls_choice" (
    "id" serial NOT NULL PRIMARY KEY,
    "choice_text" varchar(200) NOT NULL,
    "votes" integer NOT NULL
);
--
-- Create model Question
--
CREATE TABLE "polls_question" (
    "id" serial NOT NULL PRIMARY KEY,
    "question_text" varchar(200) NOT NULL,
    "pub_date" timestamp with time zone NOT NULL
);
--
-- Add field question to choice
--
ALTER TABLE "polls_choice" ADD COLUMN "question_id" integer NOT NULL;
ALTER TABLE "polls_choice" ALTER COLUMN "question_id" DROP DEFAULT;
CREATE INDEX "polls_choice_7aa0f6ee" ON "polls_choice" ("question_id");
ALTER TABLE "polls_choice"
  ADD CONSTRAINT "polls_choice_question_id_246c99a640fbbd72_fk_polls_question_id"
    FOREIGN KEY ("question_id")
    REFERENCES "polls_question" ("id")
    DEFERRABLE INITIALLY DEFERRED;

COMMIT;

Обратите внимание на следующее:

  • Точный вывод будет варьироваться в зависимости от используемой базы данных. Приведённый выше пример сгенерирован для PostgreSQL.
  • Имена таблиц автоматически генерируются путём объединения имени приложения (polls) и нижнего регистра имени модели — question и choice. (Вы можете настроить это поведение.)
  • Первичные ключи (ID) добавляются автоматически. (Вы можете настроить это тоже.)
  • По соглашению, Django добавляет "_id" к имени поля внешнего ключа. (Да, вы можете настроить это тоже.)
  • Взаимоотношения внешнего ключа явным образом указываются с помощью ограничения FOREIGN KEY. Не беспокойтесь о частях DEFERRABLE; это просто указывает PostgreSQL на то, чтобы он не принуждал внешние ключи до конца транзакции.
  • Он настраивается под используемую базу данных, поэтому такие специфичные для базы данных типы полей, как auto_increment (MySQL), serial (PostgreSQL) или integer primary key autoincrement (SQLite), обрабатываются автоматически. То же самое относится к цитированию имён полей — например, с использованием двойных или одинарных кавычек.
  • Команда sqlmigrate фактически не выполняет миграцию в вашей базе данных — она просто выводит её на экран, чтобы вы могли увидеть SQL-код, который Django считает необходимым. Это полезно для проверки того, что собирается сделать Django, или если у вас есть администраторы баз данных, которые требуют SQL-скрипты для изменений.

Если вас интересует, вы также можете запустить python manage.py check; это проверяет наличие проблем в вашем проекте без создания миграций или изменения базы данных.

Теперь снова запустите migrate, чтобы создать эти таблицы моделей в вашей базе данных:

$ python manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, polls, sessions
Running migrations:
  Rendering model states... DONE
  Applying polls.0001_initial... OK

Команда migrate берёт все миграции, которые ещё не были применены (Django отслеживает применённые с помощью специальной таблицы в вашей базе данных, называемой django_migrations), и выполняет их в вашей базе данных — по сути, синхронизирует изменения, которые вы внесли в свои модели, со схемой в базе данных.

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

  • Измените свои модели (в models.py).
  • Запустите python manage.py makemigrations, чтобы создать миграции для этих изменений.
  • Запустите python manage.py migrate, чтобы применить эти изменения к базе данных.

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

Обратитесь к документации django-admin за подробной информацией о том, что может делать утилита manage.py.

Работа с API

Теперь давайте перейдём в интерактивную оболочку Python и поиграем с бесплатным API, который предоставляет Django. Для вызова оболочки Python используйте эту команду:

$ python manage.py shell

Мы используем это вместо простого ввода «python», потому что manage.py устанавливает переменную среды DJANGO_SETTINGS_MODULE, которая предоставляет Django путь импорта Python к вашему файлу mysite/settings.py.

Обход manage.py

Если вы предпочитаете не использовать manage.py, это не проблема. Просто установите переменную среды DJANGO_SETTINGS_MODULE на mysite.settings, запустите обычную оболочку Python и настройте Django:

>>> import django
>>> django.setup()

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

Вы должны запустить python из той же директории, что и manage.py, или убедиться, что эта директория находится на пути Python, чтобы import mysite работало.

Дополнительную информацию об этом можно найти в документации django-admin.

После того как вы оказались в оболочке, изучите API базы данных:

>>> from polls.models import Question, Choice   # Import the model classes we just wrote.

# No questions are in the system yet.
>>> Question.objects.all()
<QuerySet []>

# Create a new Question.
# Support for time zones is enabled in the default settings file, so
# Django expects a datetime with tzinfo for pub_date. Use timezone.now()
# instead of datetime.datetime.now() and it will do the right thing.
>>> from django.utils import timezone
>>> q = Question(question_text="What's new?", pub_date=timezone.now())

# Save the object into the database. You have to call save() explicitly.
>>> q.save()

# Now it has an ID. Note that this might say "1L" instead of "1", depending
# on which database you're using. That's no biggie; it just means your
# database backend prefers to return integers as Python long integer
# objects.
>>> q.id
1

# Access model field values via Python attributes.
>>> q.question_text
"What's new?"
>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=<UTC>)

# Change values by changing the attributes, then calling save().
>>> q.question_text = "What's up?"
>>> q.save()

# objects.all() displays all the questions in the database.
>>> Question.objects.all()
<QuerySet [<Question: Question object>]>

Постойте. <Question: Question object> — совершенно бесполезное представление этого объекта. Давайте исправим это, отредактировав модель Question (в файле polls/models.py) и добавив метод __str__() в Question и Choice:

from django.db import models
from django.utils.encoding import python_2_unicode_compatible

@python_2_unicode_compatible  # only if you need to support Python 2
class Question(models.Model):
    # ...
    def __str__(self):
        return self.question_text

@python_2_unicode_compatible  # only if you need to support Python 2
class Choice(models.Model):
    # ...
    def __str__(self):
        return self.choice_text

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

Обратите внимание, что это обычные методы Python. Давайте добавим пользовательский метод просто для демонстрации:

import datetime

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


class Question(models.Model):
    # ...
    def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

Обратите внимание на добавление import datetime и from django.utils import timezone, чтобы сослаться на стандартный модуль Python datetime и утилиты Django, связанные с часовыми поясами, в django.utils.timezone соответственно. Если вы не знакомы с обработкой часовых поясов в Python, вы можете узнать больше в документации по поддержке часовых поясов.

Сохраните эти изменения и запустите новую интерактивную оболочку Python, снова выполнив python manage.py shell:

>>> from polls.models import Question, Choice

# Make sure our __str__() addition worked.
>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>

# Django provides a rich database lookup API that's entirely driven by
# keyword arguments.
>>> Question.objects.filter(id=1)
<QuerySet [<Question: What's up?>]>
>>> Question.objects.filter(question_text__startswith='What')
<QuerySet [<Question: What's up?>]>

# Get the question that was published this year.
>>> from django.utils import timezone
>>> current_year = timezone.now().year
>>> Question.objects.get(pub_date__year=current_year)
<Question: What's up?>

# Request an ID that doesn't exist, this will raise an exception.
>>> Question.objects.get(id=2)
Traceback (most recent call last):
    ...
DoesNotExist: Question matching query does not exist.

# Lookup by a primary key is the most common case, so Django provides a
# shortcut for primary-key exact lookups.
# The following is identical to Question.objects.get(id=1).
>>> Question.objects.get(pk=1)
<Question: What's up?>

# Make sure our custom method worked.
>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

# Give the Question a couple of Choices. The create call constructs a new
# Choice object, does the INSERT statement, adds the choice to the set
# of available choices and returns the new Choice object. Django creates
# a set to hold the "other side" of a ForeignKey relation
# (e.g. a question's choice) which can be accessed via the API.
>>> q = Question.objects.get(pk=1)

# Display any choices from the related object set -- none so far.
>>> q.choice_set.all()
<QuerySet []>

# Create three choices.
>>> q.choice_set.create(choice_text='Not much', votes=0)
<Choice: Not much>
>>> q.choice_set.create(choice_text='The sky', votes=0)
<Choice: The sky>
>>> c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# Choice objects have API access to their related Question objects.
>>> c.question
<Question: What's up?>

# And vice versa: Question objects get access to Choice objects.
>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>
>>> q.choice_set.count()
3

# The API automatically follows relationships as far as you need.
# Use double underscores to separate relationships.
# This works as many levels deep as you want; there's no limit.
# Find all Choices for any question whose pub_date is in this year
# (reusing the 'current_year' variable we created above).
>>> Choice.objects.filter(question__pub_date__year=current_year)
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# Let's delete one of the choices. Use delete() for that.
>>> c = q.choice_set.filter(choice_text__startswith='Just hacking')
>>> c.delete()

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

Представление административного интерфейса Django

Философия

Генерация сайтов администратора для вашего персонала или клиентов для добавления, изменения и удаления контента — это трудоемкая работа, которая не требует большого творческого подхода. По этой причине Django полностью автоматизирует создание интерфейсов администратора для моделей.

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

Сайт администратора не предназначен для использования посетителями сайта. Он предназначен для менеджеров сайта.

Создание пользователя администратора

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

$ python manage.py createsuperuser

Введите желаемое имя пользователя и нажмите Enter.

Username: admin

Затем вам будет предложено ввести желаемый адрес электронной почты:

Email address: admin@example.com

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

Password: **********
Password (again): *********
Superuser created successfully.

Запуск сервера разработки

Сайт Django администратора активирован по умолчанию. Давайте запустим сервер разработки и изучим его.

Если сервер не запущен, запустите его следующим образом:

$ python manage.py runserver

Теперь откройте веб-браузер и перейдите к «/admin/» на вашем локальном домене — например, http://127.0.0.1:8000/admin/. Вы должны увидеть экран входа в систему администратора:

Django admin login screen

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

Вход на сайт администратора

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

Django admin index page

Вы должны увидеть несколько типов редактируемого контента: группы и пользователи. Они предоставляются django.contrib.auth — фреймворком аутентификации, поставляемым Django.

Сделать приложение опросов изменяемым в администраторе

Но где наше приложение опросов? Оно не отображается на главной странице администратора.

Нужно сделать только одно: сказать администратору, что Question объекты имеют интерфейс администратора. Для этого откройте файл polls/admin.py и измените его, чтобы он выглядел так:

from django.contrib import admin

from .models import Question

admin.site.register(Question)

Изучение бесплатной функциональности администратора

Теперь, когда мы зарегистрировали Question, Django знает, что оно должно отображаться на главной странице администратора:

Django admin index page, now with polls displayed

Нажмите «Вопросы». Теперь вы находитесь на странице «список изменений» для вопросов. Эта страница отображает все вопросы в базе данных и позволяет выбрать один для изменения. Есть вопрос «Что нового?», который мы создали ранее:

Polls change list page

Нажмите вопрос «Что нового?» для редактирования:

Editing form for question object

Следует обратить внимание на следующие моменты:

  • Форма автоматически генерируется из модели Question.
  • Разные типы полей модели (DateTimeField, CharField) соответствуют соответствующим виджетам HTML-ввода. Каждый тип поля знает, как отобразить себя в Django администраторе.
  • Каждый DateTimeField получает бесплатные JavaScript-сокращения. Даты получают сокращение «Сегодня» и всплывающее окно календаря, а время получает сокращение «Сейчас» и удобное всплывающее окно, в котором перечислены часто вводимые времена.

В нижней части страницы предоставляется несколько вариантов:

  • Сохранить — Сохраняет изменения и возвращается к странице списка изменений для этого типа объекта.
  • Сохранить и продолжить редактирование — Сохраняет изменения и перезагружает страницу администратора для этого объекта.
  • Сохранить и добавить другой — Сохраняет изменения и загружает новую пустую форму для этого типа объекта.
  • Удалить — Отображает страницу подтверждения удаления.

Если значение «Дата публикации» не соответствует времени, когда вы создали вопрос в Уроке 1, это, вероятно, означает, что вы забыли установить правильное значение для параметра TIME_ZONE. Измените его, перезагрузите страницу и проверьте, что появляется правильное значение.

Измените «Дата публикации», щелкнув по сокращениям «Сегодня» и «Сейчас». Затем нажмите «Сохранить и продолжить редактирование». Затем нажмите «История» в правом верхнем углу. Вы увидите страницу, на которой перечислены все изменения, внесенные в этот объект через Django администратор, со временем и именем пользователя, внесшего изменение:

History page for question object

Когда вы освоитесь с API моделей и ознакомитесь с сайтом администратора, прочитайте часть 3 этого учебника, чтобы узнать, как добавить больше представлений в наше приложение опросов.

© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/1.11/intro/tutorial02/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API