воскресенье, 16 июля 2017 г.

Бэкенд для LDAP-аутентификации в Django

Написал небольшой модуль для аутентификации в Django через LDAP-сервер. Для входа нужно ввести полное имя пользователя в формате user@domain.tld и пароль пользователя.

Модуль учитывает состояние пользователя - заблокирован он в каталоге LDAP или нет. Если пользователь найден в LDAP, но отсутствует в таблице auth_user, то пользователь создаётся и помечается активным. Если пользователь не найден в LDAP (может быть не найден из-за неверного пароля или из-за того, что пользователь заблокирован), но найден в таблице auth_user, то отметка активности пользователя в таблице снимается.

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

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

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

Сохраним бэкенд в файл application/ldap_backend.py внутри каталога проекта:
# -*- coding: UTF-8 -*-

import random, ldap
from django.contrib.auth.models import User
from django.contrib.auth.hashers import make_password

# Функция, генерирующая пароль или ключ указанной сложности
def genkey(upper=True, lower=True, digits=True, signs=False, length=64):
    chars = ''

    if upper:
        chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    if lower:
        chars += 'abcdefghijklmnopqrstuvwxyz'
    if digits:
        chars += '0123456789'
    if signs:
        chars += '()[]{}<>|/~!@#$%^&,.;:*-+=_'

    key = ''
    num = len(chars)
    for i in xrange(0, length):
        j = int(random.random() * num)
        key += chars[j]
    return key

class LDAPBackend(object):
    def authenticate(self, username=None, password=None):
        # Ищем пользователя в LDAP
        try:
            login, domain = username.split('@')
        except ValueError:
            return None

        base_dn = domain.split('.')
        base_dn = map(lambda x: 'dc=' + x, base_dn)
        base_dn = ','.join(base_dn)

        l = ldap.initialize('ldap://' + domain)
        try:
            l.simple_bind_s(username, password)
        except ldap.INVALID_CREDENTIALS:
            return None
        l.set_option(ldap.OPT_REFERRALS, 0)
        result = l.search_s(base_dn,
                            ldap.SCOPE_SUBTREE,
                            '(&(sAMAccountName=%s)(objectClass=user)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))' % login,
                            None)
        found, data = result[0]
        l.unbind_s()

        # Ищем пользователя в django или создаём его
        try:
            user = User.objects.get(username=username)
        except User.DoesNotExist:
            user = User(username=username)
            user.password = make_password(genkey())
            user.is_staff = True
            user.is_superuser = False

        # Обновляем пользователя в django
        user.email = data.get('mail', [''])[0]
        user.first_name = data.get('givenName', [''])[0].decode('UTF-8') # data.get('middleName', [''])[0].decode('UTF-8')
        user.last_name = data.get('sn', [''])[0].decode('UTF-8')
        if found:
            user.is_active = True
        else:
            user.is_active = False
        user.save()

        return user

    def get_user(self, user_id):
        try:
            return User.objects.get(pk=user_id)
        except User.DoesNotExist:
            return None
Для использования этого модуля аутентификации, нужно прописать его в настройках проекта, в файле settings.py:
AUTHENTICATION_BACKENDS = (
  'application.ldap_backend.LDAPBackend',
  'django.contrib.auth.backends.ModelBackend'
)
Если стандартная аутентификация Django не требуется, то стандартный модуль можно отключить.

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

воскресенье, 9 июля 2017 г.

Обработка аутентификации в Django

Частичный перевод статьи: Handling Authentication & Authorization
Автор: Натан Йерглер (Nathan Yergler)

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

Чтобы воспользоваться встроенной поддержкой аутентификации, к проекту нужно подключить приложения django.contrib.auth и django.contrib.sessions.

Как можно увидеть в addressbook/settings.py, при создании проекта Django, они уже включены по умолчанию.
INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Раскомментируйте следующую строку, чтобы включить интерфейс администрирования:
    # 'django.contrib.admin',
    # Раскомментируйте следующую строку, чтобы включить документацию на интерфейс администрирования:
    # 'django.contrib.admindocs',
    'contacts',
)
Кроме установки приложений, нужно также установить промежуточное программное обеспечение.
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',
    # Раскомментируйте следующую строку для включения простой защиты от перехвата щелчков мыши:
    # 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
Как мы помним, во время первого запуска syncdb Django задаёт вопрос - нужно ли создать учётную запись суперпользователя. Это происходит, потому что промежуточное программное обеспечение уже установлено.

В модуле auth из стандартной поставки Django имеются модели User - пользователь, Group - группа и Permission - права. Обычно этого бывает достаточно, если не нужно интегрировать приложение с другой системой аутентификации.

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

Для рассматриваемого примера нужно просто добавить в наш проект представления login и logout. Для начала добавим представления в
файл addressbook/urls.py.
urlpatterns = patterns('',
    url(r'^login/$', 'django.contrib.auth.views.login'),
    url(r'^logout/$', 'django.contrib.auth.views.logout'),
У обоих представлений login и logout есть имена шаблонов по умолчанию (registration/login.html и registration/logged_out.html, соответственно). Поскольку эти представления специфичны для нашего проекта и не используются повторно приложением Contacts, мы создадим новый каталог templates/registration внутри каталога приложения addressbook:
$ mkdir -p addressbook/templates/registration
И сообщим Django о необходимости искать шаблоны в этом каталоге, настроив TEMPLATE_DIRS в addressbook/settings.py.
TEMPLATE_DIRS = (
    # Поместите здесь строки, такие как "/home/html/django_templates" или "C:/www/django/templates".
    # Всегда используйте прямую косую черту, даже в Windows.
    # Не забывайте, что нужно указывать полный путь, а не относительный.
    'addressbook/templates',
)
Внутри этого каталога сначала создадим файл login.html.
{% extends "base.html" %}

{% block content %}

{% if form.errors %}
<p>Ваше имя пользователя и пароль не подходят. Попробуйте ещё раз.</p>
{% endif %}

<form method="post" action="{% url 'django.contrib.auth.views.login' %}">
{% csrf_token %}
<table>
<tr>
    <td>{{ form.username.label_tag }}</td>
    <td>{{ form.username }}</td>
</tr>
<tr>
    <td>{{ form.password.label_tag }}</td>
    <td>{{ form.password }}</td>
</tr>
</table>

<input type="submit" value="Войти" />
<input type="hidden" name="next" value="{{ next }}" />
</form>
{% endblock %}
Шаблон login наследуется от шаблона base.html и отображает форму входа, переданную в шаблон из представления. Скрытое поле next позволяет представлению впоследствии перенаправить пользователя на запрошенную страницу, если при запросе этой страницы пользователь был перенаправлен на страницу входа.

Шаблон выхода - logged_out.html - значительно проще.
{% extends "base.html" %}

{% block content %}

Вы вышли!

{% endblock %}
Всё, что нужно - только сообщить пользователю, что выход завершился успешно.

Если сейчас запустить сервер разработки с помощью runserver и перейти на страницу по ссылке http://localhost:8000/login, то можно увидеть страницу входа. При попытке входа с неправильными данными можно увидеть сообщение об ошибке. Теперь давайте попробуем войти с данными суперпользователя, который был создан ранее.

Погодите, что случилось? Почему мы попали на страницу /accounts/profile? Мы не вводили такой адрес. После успешного входа представление login перенаправляет пользователя на определённый URL и по умолчанию это страница /accounts/profile. Чтобы заменить её, нужно настроить значение LOGIN_REDIRECT_URL в addressbook/settings.py так, чтобы при первом входе пользователь перенаправлялся бы на страницу со списком контактов.
LOGIN_REDIRECT_URL = '/'
Теперь, когда мы можем войти и выйти, было бы неплохо показывать в заголовке имя вошедшего пользователя и ссылки для входа/выхода. Добавим их в наш шаблон base.html, т.к. нам нужно отображать их везде.
<body>
    <div>
    {{ user }}
    {% if user.is_anonymous %}
    <a href="{% url 'django.contrib.auth.views.login' %}">Войти</a>
    {% else %}
    <a href="{% url 'django.contrib.auth.views.logout' %}">Выйти</a>
    {% endif %}
    </div>

воскресенье, 2 июля 2017 г.

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

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

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

  • Проект - совокупность приложений, имеющих общие настройки.
  • Приложение - часть проекта, выполняющая определённую логически неделимую функцию. Состоит из представлений (views), шаблонов (templates) и моделей (models).
  • Шаблон - шаблон HTML-страницы. В терминологии MVC шаблону соответствует представление.
  • Модель - средство доступа к данным.
  • Представление - связующий код между моделью и шаблоном. В терминологии MVC представлению соответствует контроллеру.
  • Маршрут - соответствие между URL'ом и представлением (контроллером в терминологии MVC), отвечающим за этот URL.
Программы, написанные с использованием Django, являются совокупностью отдельных приложений, объединённых в один проект. В отличие от многих других веб-фреймворков, в Django не используется архитектура MVC - Модель-Представление-Контроллер, вместо неё используется собственная архитектура MVT - Модель-Представление-Шаблон. Шаблон Django по функциям ближе всего к представлению в MVC, а представление Django по функциям ближе всего к контроллеру в MVC. Представления Django привязываются к определённому URL через маршруты.

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

В Debian и производных от него дистрибутивах установить Python и Django можно из репозиториев:
# apt-get install python python-django

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

Создаём проект 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

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

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

Откроем для редактирования файл app/views.py и придадим ему следующий вид:
# -*- coding: UTF-8 -*-
from django.http import HttpResponse

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

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

Теперь настроим маршрут, вызывающий это представление для 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, то косая черта добавляться не будет. Шаблоном для корня сайта является '^$', то есть соответствие пустой строке.

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

Чтобы в проекте использовалось наше приложение, его нужно подключить к проекту. Для этого закомментируем на время список стандартных приложений в файле 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',
)

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

Теперь настало время запустить сервер разработчика с только что созданным простейшим приложением:
$ ./manage.py runserver 0.0.0.0:8000
Можно попытаться зайти в браузере на страницу /hello/. У меня ссылка для открытия страницы выглядела так:
http://localhost:8000/hello/
Итак, мы создали наш первый проект на Django, который при обращении к странице /hello/ выводит надпись "Здравствуй, мир!" Возможности Django в этом проекте практически не используются - мы не использовали ни моделей, ни шаблонов, но этот проект даёт общее представление о структуре программ, написанных с использованием Django.

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

Для начала настроим каталог, в котором будут находиться шаблоны. Для этого отредактируем файл 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/, то можно увидеть текущее время на английском языке в часовом поясе Гринвичской обсерватории.

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

Чтобы время отображалось правильно - в часовом поясе сервера, пропишем в файл настроек местный часовой пояс. Для этого откроем файл dj/settings.py и пропишем в переменную TIME_ZONE значение 'Asia/Yekaterinburg':
TIME_ZONE = 'Asia/Yekaterinburg'
Чтобы время на странице отображалось в соответствии с правилами, принятыми в России, пропишем в файл настроек язык проекта. Откроем файл dj/settings.py и пропишем в переменную LANGUAGE_CODE значение 'ru-RU':
LANGUAGE_CODE = 'ru-RU'
Теперь текущее время должно отображаться на русском языке в часовом поясе, по которому живёт Уфа :)

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

Попробуем добавить страницы, которые будут вычитать из текущего времени часы, фигурирующие в 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 и увидеть получающиеся страницы.

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

Условие:
{% 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 %}
Внутри циклов существуют следующие переменные:
  • {{ forloop.counter }} - номер итерации, начиная с 1,
  • {{ forloop.counter0 }} - номер итерации, начиная с 0,
  • {{ forloop.revcounter }} - количество оставшихся итераций, включая текущую. Внутри первой итерации равно количеству элементов, на последней итерации - 1,
  • {{ forloop.revcounter }} - количество оставшихся итераций. Внутри первой итерации содержит количество элементов за минус единицей, на последней итерации - 0,
  • {% if forloop.first %} - условие выполняется на первой итерации,
  • {% if forloop.last %} - условие выполняется на последней итерации,
  • forloop.parentloop - ссылка на объект родительского цикла.
Комментарии в шаблоне:
{# Это комментарий #}
или
{% comment %}
Многострочный
комментарий.
{% endcomment %}

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

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

Включение подшаблона из файла:
{% include "includes/nav.html" %}
Включение подшаблона из переменной:
{% include template_name %}

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

Наследование шаблонов - очень удобная концепция, которая встречается не во всех шаблонизаторах. В шаблонизаторе 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 %}
В производном шаблоне указывается базовый шаблон и переопределяется содержимое блоков базового шаблона. Шаблоны других страниц можно отредактировать сходным образом, чтобы и они использовали дизайн, общий для всех страниц.

Настройка базы данных 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()
Если сообщений об ошибках не было, значит всё настроено правильно.

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

Каждая модель 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

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

Откроем оболочку 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()
Недостаток такого рода редактирования объектов заключается в том, что в базе данных обновляются все поля в отредактированной строке таблицы, а не только то поле, которое действительно было изменено. Для раздельного редактирования полей строк можно воспользоваться массовым редактированием, о котором будет рассказано далее.

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

Откроем оболочку Python:
$ ./manage.py shell
Импортируем описание модели:
from address.models import *
Загрузим все объекты типа "город":
c = City.objects.all()
Загрузим объект "город", имеющий имя "г. Уфа":
c = City.objects.filter(title=u'г. Уфа')
И убедимся, что загрузился именно он:
print c
Выбор списка объектов:
  • Obj.objects.all() - отобрать как список все объекты типа Obj,
  • Obj.objects.filter(field='') - отобрать как список объекты, у которых поле field имеет указанное значение,
  • Obj.objects.filter(field1='', field2='') - отобрать как список объекты, у которых оба поля одновременно имеют указанные значения,
  • Obj.objects.filter(field__contains='') - отобрать как список объекты, у которых в указанном поле содержится указанное значение,
  • Obj.objects.filter(field__icontains='') - отобрать как список объекты, у которых в указанном поле содержится указанное значение без учёта регистра,
  • Obj.objects.filter(field__iexact='') - отобрать как список объекты, у которых указанное поле совпадает с указанным значением без учёта регистра,
  • Obj.objects.filter(field__startswith='') - отобрать как список объекты, у которых указанное поле начинается с указанного значения,
  • Obj.objects.filter(field__istartswith='') - отобрать как список объекты, у которых указанное поле начинается с указанного значения без учёта регистра,
  • Obj.objects.filter(field__endswith='') - отобрать как список объекты, у которых указанное поле заканчивается указанным значением,
  • Obj.objects.filter(field__iendswith='') - отобрать как список объекты, у которых указанное поле заканчивается указанным значением без учёта регистра.
Условия в фильтре можно комбинировать между собой не только указывая их через запятую, но создавая каскады фильтров:
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. Стоит учитывать, что этот метод никак не обрабатывает случаи, когда условию соответствует несколько записей сразу.

Сортировка данных:
  • Obj.objects.order_by("field") - сортировка по одному полю,
  • Obj.objects.order_by("-field") - сортировка по одному полю в обратном порядке,
  • Obj.objects.order_by("field1", "field2") - сортировка по двум полям.
Сортировку по умолчанию можно настроить в свойствах объекта модели, добавив вложенный класс со свойством 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()

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

Пожалуй самая приятная особенность фреймворка 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 для конструирования более сложных запросов, не рассмотрены формы и модельные формы, не рассмотрена миграция структуры базы данных при изменении моделей и многое другое.