Краткий учебник по Django

Не так давно нашёл файл с записями, которые я делал в 2012 году, когда осваивал веб-фреймворк Django. Я подумал, что не стоит зря пропадать этим записям и решил выложить их здесь. Хотя в этом файле есть что дополнить, в процессе подготовки публикации радикальных правок в этот файл я вносить не стал, т.к. фреймворк очень большой и начав раскрывать частности, легко раздуть и без того большой файл до невероятных размеров.

1. О терминологии веб-фреймворка Django

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

2. Установка Python и Django

В Debian и производных от него дистрибутивах установить Python и Django можно из репозиториев:

# apt-get install python python-django

3. Создание проекта и приложения

Создаём проект dj:

$ django-admin startproject dj

Переходим в каталог проекта:

$ cd dj

Смотрим содержимое каталога проекта:

$ find .
.
./manage.py
./dj
./dj/wsgi.py
./dj/settings.py
./dj/__init__.py
./dj/urls.py

Создаём в проекте новое приложение app:

$ ./manage.py startapp app

В каталоге проекта появится новый каталог с именем app, созданный специально для размещения приложения:

$ find .
.
./manage.py
./dj
./dj/wsgi.py
./dj/settings.py
./dj/__init__.py
./dj/urls.py
./dj/settings.pyc
./dj/__init__.pyc
./app
./app/tests.py
./app/views.py
./app/models.py
./app/__init__.py

4. Создание представления

Создадим новое представление hello в приложении app. Представление принимает в качестве аргумента объект HttpRequest и возвращает объект HttpResponse.

Откроем для редактирования файл app/views.py и придадим ему следующий вид:

# -*- coding: UTF-8 -*-
from django.http import HttpResponse

def hello(request):
    return HttpResponse(u'Здравствуй, мир!')

5. Создание маршрута

Теперь настроим маршрут, вызывающий это представление для url. Для этого откроем файл dj/urls.py и добавим в него пару строчек.

Первую строчку добавим в начало файла, после других строчек импорта. Строчка импортирует представление hello из приложения app:

from app.views import hello

Теперь найдём функцию patterns, возвращаемое значение которой присваивается переменной urlpatterns, и впишем в аргументы функции следующую строчку:

('^hello/$', hello),

В итоге у меня содержимое файла dj/urls.py приняло следующий вид:

from django.conf.urls import patterns, include, url
from app.views import hello

# Uncomment the next two lines to enable the admin:
# from django.contrib import admin
# admin.autodiscover()

urlpatterns = patterns('',
    # Examples:
    # url(r'^$', 'dj.views.home', name='home'),
    # url(r'^dj/', include('dj.foo.urls')),

    # Uncomment the admin/doc line below to enable admin documentation:
    # url(r'^admin/doc/', include('django.contrib.admindocs.urls')),

    # Uncomment the next line to enable the admin:
    # url(r'^admin/', include(admin.site.urls)),
    ('^hello/$', hello),
)

В шаблоне url в конце обязательно должна быть косая черта, т.к. если клиент запросит страницу без косой черты в конце, Django автоматически добавит её и попытается найти представление, соответствующее этому URL. Нужно ли добавлять косую черту, регулируется настройкой APPEND_SLASH. Если установить её в False, то косая черта добавляться не будет. Шаблоном для корня сайта является '^$', то есть соответствие пустой строке.

6. Включение приложения в проекте

Чтобы в проекте использовалось наше приложение, его нужно подключить к проекту. Для этого закомментируем на время список стандартных приложений в файле dj/settings.py и пропишем наше приложение:

INSTALLED_APPS = (
    #'django.contrib.auth',
    #'django.contrib.contenttypes',
    #'django.contrib.sessions',
    #'django.contrib.sites',
    #'django.contrib.messages',
    #'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'app',
)

Также закомментируем в настройках подключение приложений-прослоек:

MIDDLEWARE_CLASSES = (
    #'django.middleware.common.CommonMiddleware',
    #'django.contrib.sessions.middleware.SessionMiddleware',
    #'django.middleware.csrf.CsrfViewMiddleware',
    #'django.contrib.auth.middleware.AuthenticationMiddleware',
    #'django.contrib.messages.middleware.MessageMiddleware',
    # Uncomment the next line for simple clickjacking protection:
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)

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

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

$ ./manage.py runserver 0.0.0.0:8000

Можно попытаться зайти в браузере на страницу /hello/. У меня ссылка для открытия страницы выглядела так:

http://localhost:8000/hello/

Итак, мы создали наш первый проект на Django, который при обращении к странице /hello/ выводит надпись "Здравствуй, мир!" Возможности Django в этом проекте практически не используются - мы не использовали ни моделей, ни шаблонов, но этот проект даёт общее представление о структуре программ, написанных с использованием Django.

8. Использование шаблонов

Для начала настроим каталог, в котором будут находиться шаблоны. Для этого отредактируем файл dj/settings.py и впишем в кортеж TEMPLATES_DIRS полный путь к каталогу с шаблонами.

После редактирования файла эта настройка в файле settings.py приняла следующий вид:

TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
    # Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    "/home/stupin/dj/templates",
)

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

Теперь создадим каталог для шаблонов:

$ mkdir templates

И создадим в нём новый шаблон с именем time.tmpl и со следующим содержимым:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Текущее время</title>
  </head>
  <body>
    <div align="center">
      Сейчас {{ time }}
    </div>
  </body>
</html>

Теперь добавим в файл app/views.py импорт функции для загрузки и отрисовки шаблона:

from django.shortcuts import render_to_response

И создадим представление current_datetime следующим образом:

def current_datetime(request):
    return render_to_response('time.tmpl', {'time' : datetime.now()})

Осталось настроить в файле urls.py маршрут к этому представлению:

('^time/$', current_datetime),

Если сейчас попробовать открыть страницу http://localhost:800/time/, то можно увидеть текущее время на английском языке в часовом поясе Гринвичской обсерватории.

9. Настройка часового пояса и языка

Чтобы время отображалось правильно - в часовом поясе сервера, пропишем в файл настроек местный часовой пояс. Для этого откроем файл dj/settings.py и пропишем в переменную TIME_ZONE значение 'Asia/Yekaterinburg':

TIME_ZONE = 'Asia/Yekaterinburg'

Чтобы время на странице отображалось в соответствии с правилами, принятыми в России, пропишем в файл настроек язык проекта. Откроем файл dj/settings.py и пропишем в переменную LANGUAGE_CODE значение 'ru-RU':

LANGUAGE_CODE = 'ru-RU'

Теперь текущее время должно отображаться на русском языке в часовом поясе, по которому живёт Уфа :)

10. Пример более сложного маршрута

Попробуем добавить страницы, которые будут вычитать из текущего времени часы, фигурирующие в URL запрошенной страницы. Для этого в файл dj/urls.py добавим маршруты:

(r'^time/plus/(\d{1,2})$', hours_plus),
(r'^time/minus/(\d{1,2})$', hours_minus),

В каталоге с шаблонами templates разместим два новых шаблона.

Файл templates/time_minus.tmpl со следующим содержимым:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Прошлое время</title>
  </head>
  <body>
    <div align="center">
      {{ delta }} часов назад было {{ time }}
    </div>
  </body>
</html>

Файл templates/time_plus.tmpl со следующим содержимым:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Будущее время</title>
  </head>
  <body>
    <div align="center">
      Через {{ delta }} часов будет {{ time }}
    </div>
  </body>
</html>

В файл app/views.py пропишем два представления, которые будут использовать два новых шаблона:

def hours_plus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() + timedelta(hours=delta)
    return render_to_response('time_plus.tmpl', {'delta' : delta,
                                                 'time' : time})

def hours_minus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() - timedelta(hours=delta)
    return render_to_response('time_minus.tmpl', {'delta' : delta,
                                                  'time' : time})

Файл app/views.py целиком примет следующий вид:

# -*- coding: UTF-8 -*-
from django.http import HttpResponse, Http404
from django.shortcuts import render_to_response
from datetime import datetime, timedelta

def hello(request):
    return HttpResponse(u'Здравствуй, мир!')

def current_datetime(request):
    return render_to_response('time.tmpl', {'time' : datetime.now()})

def hours_plus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() + timedelta(hours=delta)
    return render_to_response('time_plus.tmpl', {'delta' : delta,
                                                 'time' : time})

def hours_minus(request, delta):
    try:
      delta = int(delta)
    except ValueError:
      raise Http404()
    time = datetime.now() - timedelta(hours=delta)
    return render_to_response('time_minus.tmpl', {'delta' : delta,
                                                  'time' : time})

Теперь можно перейти по ссылкам http://localhost:8000/time/plus/1 или http://localhost:8000/time/plus/2 и увидеть получающиеся страницы.

11. Более сложные шаблоны

Условие:

{% if today_is_weekend %}
    <p>Сегодня выходной!</p>
{% endif %}

Условие с двумя вариантами:

{% if today_is_weekend %}
    <p>Сегодня выходной!</p>
{% else %}
    <p>Пора работать.</p>
{% endif %}

Ложным значениями являются: пустой список [], пустой кортеж (), пустой словарь {}, ноль - 0, объект None и объект False.

Можно использовать сочетания условий при помощи and и or, причём and имеет более высокий приоритет. Скобки в условиях не поддерживаются, без них можно обойтись с помощью вложенных условий. Также возможно использовать операторы ==, !=, <, >, >=, <= и in для вычисления условий, по смыслу совпадающих с условными операторами в самом Python.

Циклы для перебора значений из списка:



		
{% for athlete in athlete_list %}
  <li>{{ athlete.name }}</li>
{% endfor %}

Можно перебирать значения из списка в обратном порядке:

{% for athlete in athlete_list reversed %}
...
{% endfor %}

Внутри циклов существуют следующие переменные:

Комментарии в шаблоне:

{# Это комментарий #}

или

{% comment %}
Многострочный
комментарий.
{% endcomment %}

12. Включение подшаблонов

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

Включение подшаблона из файла:

{% include "includes/nav.html" %}

Включение подшаблона из переменной:

{% include template_name %}

13. Наследование шаблонов

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

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

Базовый шаблон base.tmpl:

<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>{% block title %}{% endblock %}</title>
  </head>
  <body>
    <h1>Бесполезный сайт с часами</h1>

    {% block content %}{% endblock %}

    {% block footer %}
    <hr>
    <p>Благодарим за посещение нашего сайта.</p>
    {% endblock %}
  </body>
</html>

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

Производный шаблон. Для примера приведу только содержимое шаблона time_plus.tmpl:

{% extends "base.tmpl" %}

{% block title %}Будущее время{% endblock %}

{% block content %}
<p>Через {{ delta }} часов будет {{ time }}</p>
{% endblock %}

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

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

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

Установим модуль для доступа к базе данных MySQL из Python (разработчики фреймворка Django рекомендуют использовать PostgreSQL, но мы воспользуемся MySQL, поддержка которого тоже имеется):

# apt-get install python-mysqldb

Настроим кодировку сервера MySQL и порядок сортировки. Для этого в файле /etc/mysql/my.cnf в секцию [mysqld] впишем следующие настройки:

character_set_server=utf8
collation_server=utf8_unicode_ci

Перезапустим сервер базы данных:

# /etc/init.d/mysql restart

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

mysql> show variables like 'coll%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_unicode_ci |
| collation_server     | utf8_unicode_ci |
+----------------------+-----------------+
3 rows in set (0.00 sec)

mysql> show variables like 'char%';
+--------------------------+----------------------------+
| Variable_name            | Value                      |
+--------------------------+----------------------------+
| character_set_client     | utf8                       |
| character_set_connection | utf8                       |
| character_set_database   | utf8                       |
| character_set_filesystem | binary                     |
| character_set_results    | utf8                       |
| character_set_server     | utf8                       |
| character_set_system     | utf8                       |
| character_sets_dir       | /usr/share/mysql/charsets/ |
+--------------------------+----------------------------+
8 rows in set (0.00 sec)

Создадим базу данных для проекта:

CREATE USER app@localhost IDENTIFIED BY "app_password";
CREATE DATABASE app;
GRANT ALL ON app.* TO app@localhost;

Пропишем в файл настроек проекта dj/settings.py настройки подключения к базе данных:

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'.
        'NAME': 'app',                        # Or path to database file if using sqlite3.
        'USER': 'app',                        # Not used with sqlite3.
        'PASSWORD': 'app_password',           # Not used with sqlite3.
        'HOST': '',                           # Set to empty string for localhost. Not used with sqlite3.
        'PORT': '',                           # Set to empty string for default. Not used with sqlite3.
    }
}

Проверяем правильность настройки:

$ ./manage.py shell

Если всё правильно, то откроется отладочная оболочка Python. Попробуем ввести следующие команды:

from django.db import connection
cursor = connection.cursor()

Если сообщений об ошибках не было, значит всё настроено правильно.

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

Каждая модель Django описывают одну таблицу в базе данных. В Django имеется система отображения таблиц в объекты - ORM - Object-Relational Mapping - объектно-реляционное отображение. ORM позволяет манипулировать единичными строками таблиц как объектами Python, а также осуществлять операции массовой выборки, обновления и удаления строк.

В качестве примера, рассмотрим адресный справочник домов в городах. Откроем файл app/models.py и приведём его к следующему виду:

# -*- coding: UTF-8 -*-
from django.db import models

class City(models.Model):
    country = models.ForeignKey(Country)
    title = models.CharField(max_length=150)

class Street(models.Model):
    city = models.ForeignKey(City)
    title = models.CharField(max_length=150)

class Area(models.Model):
    city = models.ForeignKey(City)
    title = models.CharField(max_length=150)

class House(models.Model):
    area = models.ForeignKey(Area)
    street = models.ForeignKey(Street)
    house = models.IntegerField()
    frac = models.CharField(max_length=30)
    comment = models.CharField(max_length=100)

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

В Django есть не только текстовые поля и внешние ключи. Имеются числовые поля, списковые поля, логические поля, поля для связей один-к-одному и для связей многие-ко-многим. У полей можно прописать значения по умолчанию, разрешить или запретить использовать значение NULL и запретить или разрешить вводить в строковые поля пустые строки. У поля можно прописать его имя в базе данных, выставить признак - нужно ли создавать индекс по этому полю, можно прописать текстовое описание поля, которое будет использоваться в веб-интерфейсе администратора и в объектах форм Django. У классов моделей, в свою очередь, можно тоже указывать их текстовые описания, прописывать составные индексы, ограничения уникальности записей и т.п. Создание моделей - это обширная тема, рассмотреть которую сколь-нибудь подробно в рамках этого небольшого учебника вряд ли получится.

Теперь мы можем проверить правильность синтаксиса и логики моделей:

$ ./manage.py validate

Чтобы увидеть команды SQL для создания структуры базы данных, требуемой для моделей из приложения app, введём следующую команду:

$ ./manage.py sqlall app

Чтобы выполнить эти операторы SQL и создать в базе данных таблицы, соответствующие моделям, нужно выполнить следующую команду:

$ ./manage.py syncdb

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

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

$ ./manage.py dbshell

16. Создание записей в таблицах

Откроем оболочку Python:

$ ./manage.py shell

Импортируем описание моделей:

from address.models import *

Создадим объект "город":

c = City(title=u'Уфа')

И сохраним его в базу данных:

c.save()

Теперь создадим объект "улица" в этом городе:

s = Street(title=u'ул. Карла Маркса', city=c)

И сохраним объект с улицей:

s.save()

Если нужно отредактировать объект, то можно прописать в него новое свойство и сохранить:

c.title = u'г. Уфа'
c.save()

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

17. Извлечение записей из таблиц

Откроем оболочку Python:

$ ./manage.py shell

Импортируем описание модели:

from address.models import *

Загрузим все объекты типа "город":

c = City.objects.all()

Загрузим объект "город", имеющий имя "г. Уфа":

c = City.objects.filter(title=u'г. Уфа')

И убедимся, что загрузился именно он:

print c

Выбор списка объектов:

Условия в фильтре можно комбинировать между собой не только указывая их через запятую, но создавая каскады фильтров:

Obj.objects.filter(field1__iendswith='a').filter(field2='b')

Чтобы отобрать объекты, не подходящие под указанное условие, можно воспользоваться методом фильтрации exclude. Например, следующее выражение отберёт те записи, у которых начало первого поля без учёта регистра совпадает с a, а второе поле не равно b:

Obj.objects.filter(field1__iendswith='a').exclude(field2='')

Выбор одного объекта осуществляется точно таким же способом, как выбор списка, за исключением того, что вместо метода filter используется метод get:

Obj.objects.get(field='')

Если ни один объект не был найден, будет сгенерировано исключение Obj.DoesNotExist. Если указанным критериям соответствует несколько записей, то будет сгенерировано исключение Obj.MultipleObjectsReturned.

Как и в случае выборки списка объектов, можно комбинировать фильтры друг с другом. Но для выборки одного объекта последним методом в цепочке должен быть get. Например, вот так:

Obj.objects.filter(field1__iendswith='a').exclude(field2='').get()

Или, что то же самое, вот так:

Obj.objects.exclude(field2='').get(field1__iendswith='a')

В Django версий 1.6 и более поздних имеется метод first(), не принимающий аргументов, который возвращает первую запись из списка, если она есть. В противном случае возвращается None. Стоит учитывать, что этот метод никак не обрабатывает случаи, когда условию соответствует несколько записей сразу.

Сортировка данных:

Сортировку по умолчанию можно настроить в свойствах объекта модели, добавив вложенный класс со свойством ordering, которому присвоен список полей для сортировки:

    class Meta:
        ordering = ["field"]

Можно комбинировать методы:

Obj.objects.filter(field="").order_by("-field")

Можно выбирать необходимый фрагмент списка объектов. Например, вот этот запрос вернёт два первых объекта:

Obj.objects.filter(field="").order_by("-field")[0:2]

Массовое обновление объектов осуществляется следующим образом:

Obj.objects.filter(id=52).update(field='')

Запрос возвращает количество обновлённых строк таблицы.

Удалить один объект можно следующим образом:

o = Obj.objects.get(field='')
o.delete()

Удалить объекты массово можно так:

Obj.objects.filter(field='').delete()

18. Активация интерфейса администратора

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

Для включения интерфейса администрирования нужно внести изменения в файл настроек dj/settings.py:

  1. Вписать в INSTALLED_APPS приложения django.contrib.admin, django.contrib.auth, django.contrib.sessions и django.contrib.contenttypes,
  2. Вписать в MIDDLEWARE_CLASSES приложения-прослойки django.middleware.common.CommonMiddleware, django.contrib.sessions.middleware.SessionMiddleware, django.contrib.auth.middleware.AuthenticationMiddleware и django.contrib.messages.middleware.MessageMiddleware.

Вносим изменения в базу данных, чтобы в ней создались таблицы, необходимые для работы интерфейса администрирования:

$ ./manage.py syncdb

Если при этом отказаться от создания суперпользователя, то потом его можно создать с помощью команды:

$ ./manage.py createsuperuser

Теперь в начало файла dj/urls.py добавим использование модуля и его инициализацию:

from django.contrib import admin
admin.autodiscover()

И пропишем маршрут к интерфейсу администрирования:

urlpatterns = patterns('',
    # ...
    (r'^admin', include(admin.site.urls)),
    # ...
)

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

from app.models import *
from django.contrib import admin

admin.site.register(City)
admin.site.register(Area)
admin.site.register(Street)
admin.site.register(House)

После этого можно перейти по ссылке http://localhost:8000/admin/, войти под учётными данными, указанными команде createsuperuser, и пользоваться интерфейсом администрирования для добавления, редактирования и удаления записей в таблицах.

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

За рамками этого учебника также остались подробности описания моделей, выражения Q и F для конструирования более сложных запросов, не рассмотрены формы и модельные формы, не рассмотрена миграция структуры базы данных при изменении моделей и многое другое.

Написать автору