Создадим и переключимся в ветку users.
Перед тем, как создавать приложение, создадим темплейт для сайта - base.html и поместим его в директорию /src/templates. Для удобства редактирования темплейтов рекомендую Django Editor - plugin for Eclipse.

В нем прописаны некоторые темплейты для тегов django (вызываются по ctrl+space). Для редактирования html, просто открываем файл в html редакторе aptana.
Для изменения названия сайта (он по умолчанию создается при первом syncdb) создадим файл в корне src - install.py:
- # -*- mode: python; coding: utf-8; -*-
- from django.core.management import setup_environ
- try:
- import settings.development as settings
- except ImportError:
- import settings.production as settings
- setup_environ(settings)
- from django.contrib.sites.models import Site
- s = Site.objects.get(pk=1)
- s.domain = "musicmans.ru"
- s.name = "Меломаны"
- s.save()
и запустим выполнение (правой кнопкой на файле - Run As - Python Run). Пропишем SITE_ID=1 в настройках.
Для того, чтобы переменные настроек 'STATIC_URL', 'DEBUG' (и другие в будущем), а также имя и домен сайта были доступны в шаблонах (я их использую в base.html) напишем свои контекстные процессоры для темплейтов. Для этого создадим пакет питона (new - pydev package) в /src/, назовем, например, apps.djutils. В нем мы будем собирать все дополнительную функциональность проекта, которая может пригодиться и в будущем.
Создадим модуль питона (new - pydev module) в этом пакете под названием context_processors, следующим содержимым:
- from django.contrib.sites.models import Site, RequestSite
- def current_site(request):
- try:
- current_site = Site.objects.get_current()
- except Site.DoesNotExist:
- current_site = RequestSite(request)
- return {
- 'SITE_NAME': current_site.name,
- 'SITE_DOMAIN': current_site.domain,
- }
- def settings_processor(*settings_list):
- def _processor(request):
- from django.conf import settings
- settings_dict = {}
- for setting_name in settings_list:
- settings_dict[setting_name] = getattr(settings, setting_name)
- return settings_dict
- return _processor
- dj_settings = settings_processor(
- 'STATIC_URL', 'DEBUG'
- )
(Не пугайтесь, Site.objects.get_current() кешируется)
В /settings/common.py добавим:
- TEMPLATE_CONTEXT_PROCESSORS = (
- "django.contrib.auth.context_processors.auth",
- "django.core.context_processors.debug",
- "django.core.context_processors.i18n",
- "django.core.context_processors.media",
- "django.contrib.messages.context_processors.messages",
- "djutils.context_processors.dj_settings",
- "djutils.context_processors.current_site",
- )
Теперь в любом темплейте, использующим RequestContext, мы получаем значение вышеуказанных переменных.
Вернемся к шаблону base.html. Код шаблона приводить не буду из-за размеров. Его примерное содержание можно подсмотреть здесь. К нему простенький css. Для того, чтобы css отдавался как статика при разработке на встроенном веб-сервере django, пропишем в urls.py:
- from django.conf import settings
- if settings.DEBUG:
- urlpatterns += patterns('',
- (r'^media/(?P<path>.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
- )
- </path>
а в общие настройки пропишем ADMIN_MEDIA_PREFIX="admin", ибо media по умолчанию занята ADMIN_MEDIA_PREFIX и в случае, если мы ее не переопределим, наша статика работать не будет.
Для сжатия css, а также для перезагрузки закешированного браузером css файла в случае его обновления установим приложение django-compressor:
- $ sudo pip install BeautifulSoup
- $ sudo apt-get install git-core
- $ sudo pip install git+git://github.com/mintchaos/django_compressor@9b6966260398ff2dbdd11275e083e028e73c7af8#egg=django_compressor
(Чтобы установить последнюю версию из репозитория удалите @9b6966260398ff2dbdd11275e083e028e73c7af8 , на данный момент это как раз последний коммит.)
Добавим в requirements.txt
BeautifulSoup==3.1.0.1
git+git://github.com/mintchaos/django_compressor@9b6966260398ff2dbdd11275e083e028e73c7af8#egg=django_compressor
и в приложения в settings - compressor.
Добавим в настройки:
- COMPRESS = True
- COMPRESS_URL = STATIC_URL
- COMPRESS_ROOT = STATIC_ROOT
- COMPRESS_CSS_FILTERS = [
- 'compressor.filters.cssmin.CSSMinFilter'
- ]
- COMPRESS_JS_FILTERS = [
- 'compressor.filters.jsmin.JSMinFilter'
- ]
В случае отсутствия переменной COMPRESS в настройках проекта - приложением используется переменная DEBUG, поэтому, если вы хотите отключить сжатие на время разработки, просто закомментируйте COMPRESS.
Используем встроенные в приложения фильтры. Также можно использовать фильтры от yahoo или google.
После того как приложение создаст каталог CACHE, добавим его в svn:ignore.
Теперь попробуем использовать этот шаблон. Для начала отключим MAINTENANCE_MODE.
Исправим файл url.py
- from django.conf.urls.defaults import *
- from views import home_page
- urlpatterns = patterns('',
- url(r'^$', home_page, name="home"),
- )
Создадим файл /src/view.py для проекта:
- # -*- mode: python; coding: utf-8; -*-
- from annoying.decorators import render_to
- @render_to('homepage.html')
- def home_page(request):
- return {}
В этом виде используется декоратор функции @render_to. Он поставляется с приложением django-annoying. Установим, добавим в requirements.txt, просмотрим список возможностей (AutoOneToOne field кстати нам пригодится в приложении users).
homepage.html пока содержит следующие вещи:
- {% extends "base.html" %}
- {% load i18n %}
- {% block title %}{% trans "Главная страница" %}{% endblock %}
При создании темплейтов сразу закладываем возможность будущей интернационализации.
Для работы с html кодом используем firebug и HTML VALIDATOR. Также я использовал тег {% spaceless %} в base.html, чтобы сжать выдаваемый html.
Итак, вернемся к users. Приложение users будет хранить дополнительные поля профилей, и использовать сторонние приложения для регистрации и авторизации по open id. Создадим приложение:

Переместим его в apps и создадим модель, например, такую:
- # -*- coding:utf-8 -*-
- from django.db import models
- from django.contrib.auth.models import User
- from django.utils.translation import ugettext_lazy as _
- from annoying.fields import AutoOneToOneField#@UnresolvedImport
- GENDER_CHOICES = (
- ('M', 'Мужской'),
- ('F', 'Женский'),
- )
- class UserProfile(models.Model):
- user = AutoOneToOneField(User, related_name='user_profile', primary_key=True)
- date_birth = models.DateField(verbose_name=_(u'Дата Рождения'), blank=True, null=True)
- gender = models.CharField(verbose_name=_(u'Пол'), max_length=1, choices=GENDER_CHOICES, blank=True, null=True)
- URL = models.URLField(max_length=150, verbose_name=_(u'Ваш сайт'), blank=True, null=True, verify_exists=False)
- ICQ = models.CharField(max_length=30, verbose_name=u'ICQ', blank=True, null=True)
- skype = models.CharField(max_length=100, verbose_name=u'skype', blank=True, null=True)
- jabber = models.CharField(max_length=100, verbose_name=u'jabber', blank=True, null=True)
- mobile = models.CharField(max_length=100, verbose_name=_(u'Мобильный телефон'), blank=True, null=True)
- about = models.TextField(verbose_name=_(u'О себе'), help_text=_(u'Несколько слов о себе.'), blank=True, null=True)
- count_login = models.IntegerField(default=0)
- last_activity_ip = models.IPAddressField(null=True)
- last_activity_date = models.DateTimeField(null=True)
- class Meta:
- verbose_name = _(u'Профиль пользователя')
- verbose_name_plural = _(u'Профили пользователей')
Добавим в installed apps 'users'. Создаем первоначальную миграцию для приложения users (вызов custom command manage.py, см. изображение выше):

Можно ознакомиться с содержимым users/migrations. Для создания таблицы, вместо syncdb запускаем migrate users. После изменения модели запускаем schemamigration users --auto и снова migrate users для изменения базы.
Итак, профили у нас есть, приступим к регистрации.
$pip install hg+http://bitbucket.org/ubernostrum/django-registration@d36a38202ee3#egg=django-registration
обновляем hg+http://bitbucket.org/ubernostrum/django-registration@d36a38202ee3#egg=django-registration в requirements.txt.
Читаем документацию (быстрый старт).
Добавляем registration в приложения. Добавляем в настройки ACCOUNT_ACTIVATION_DAYS = 3.
Необходимые темплейты для приложения:
**registration/registration_form.html**
**registration/registration_complete.html**
**registration/activate.html**
**registration/activation_complete.html**
**registration/activation_email_subject.txt**
**registration/activation_email.txt**
Вот здесь можно посмотреть пример темплейта (на другое смотреть не надо, сам механизм работы приложения существенно изменился). Создадим их в директории users/templates/users/ .
Хотел перенести все темплейты туда, не вышло, темплейт e-mail'а, отсылаемого при регистрации прописан жестко, а также жестко они прописаны в тестах django-registration (структура ниже).

Теперь добавим в urls.py сайта
- urlpatterns = patterns('',
- url(r'^$', home_page, name="home"),
- (r'^users/auth/', include('registration.backends.default.urls')),
Пробуем зайти по адресу http://localhost:8000/users/auth/register/ .
Идея такова: для простой регистрации и регистрации по openid создадим свои backend'ы.
Для начала напишем backend для простой регистрации. Наследуем класс дефолтного бекенда в наше приложение users, копируем urls.py и правим маски url'ов в файле urls.py сайта и бекенда.
__init__.py бекенда:
- from registration.backends.default import DefaultBackend#@UnresolvedImport
- from users.forms import DJRegistrationForm#@UnresolvedImport
- class DjBackend(DefaultBackend):
- def get_form_class(self, request):
- """
- Return the default form class used for user registration.
- """
- return DJRegistrationForm
Код формы регистрации см. ниже.
Все работает но не все устраивает. Для начала мне не нравится длина input. Переопределим аттрибуты виджета, переопределением форм. Создадим файл forms.py в приложении users (код ниже).
Далее. Так как приложение использует отправку почты по smtp нам на данном этапе неплохо было бы его отслеживать. Можно запустить тестовый smtp сервер python (python -m smtpd -n -c DebuggingServer localhost:1025), но я предлагаю пойти другим путем.
Существует замечательное приложение django-mailer, которое собирает почту в базе, а отправляет по крону. Это нам гарантирует доставку почты, а также избавляет от ошибок при отсутствии доступа к smtp серверу. Его мы добавим в общие настройки.
Итак, устанавливаем.
$pip install git+git://github.com/jtauber/django-mailer@eb236b23a597753a0662290bc3b2666882515791#eggs=django-mailer
requirements.txt не забываем.
А теперь используем новую возможность django-1.2 - EMAIL_BACKENDS. Пропишем в настройках:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Добавляем в INSTALLED_APPS, синхронизируем базу.
Пробуем регистрироваться и ищем сериализованный объект сообщения в базе.
Если все работает, добавляем EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' в settings/development.py и наблюдаем тело письма в консоли eclipse.
Не забудем поменять LOGIN_URL и LOGIN_REDIRECT_URL:
LOGIN_URL = "/users/auth/login/"
LOGIN_REDIRECT_URL = "/"
Перейдем к редактированию профиля.
Для редактирования профиля нам потребуется inline formsets.
views.py для users с одной фукнцией для самостоятельного написания:
- @login_required
- @render_to('users/edit_profile.html')
- def edit_profile(request):
urls.py также самый обычный, самостоятельно.
Так как рендериг формсета и других форм по умолчанию нас не устраивает, создадим два подключаемых темплейта в директории djutils/templates/forms_render:
formset_table.html
- {{ formset.management_form }}
- {% for form in formset.forms %}
- {% include "forms_render/form_table.html" %}
- {% endfor %}
form_table.html
- table
- {% for field in form %}
- {% if not field.is_hidden %}
- <span class="arial-bold-90">{{ field.label_tag }}</span>{{ field }}{{ field.errors }}
- <div class="hint">{{ field.help_text }}</div>
- {% else %}
- {{ field }}
- {% endif %}
- {% endfor %}
- /table
table пришлось оставить без кавычек, иначе blogspot выводит нечто непонятное.
Соответственно,
- {% include "forms_render/formset_table.html" %}
в темплейте users/edit_profile.html .
forms.py для приложения users получился такой:
- # -*- coding: utf-8 -*-
- from django.forms import ModelForm
- from django import forms
- from django.utils.translation import ugettext_lazy as _
- from django.contrib.auth import forms as auth_forms
- from django.contrib.auth.models import User
- from users.models import UserProfile#@UnresolvedImport
- from registration.forms import RegistrationFormUniqueEmail#@UnresolvedImport
- class DJRegistrationForm(RegistrationFormUniqueEmail):
- def __init__(self, *args, **kwargs):
- super(DJRegistrationForm, self).__init__(*args, **kwargs)
- self.fields['username'].widget.attrs["size"] = 65
- self.fields['email'].widget.attrs["size"] = 65
- self.fields['password1'].widget.attrs["size"] = 65
- self.fields['password2'].widget.attrs["size"] = 65
- class AuthForm(auth_forms.AuthenticationForm):
- def __init__(self, *args, **kwargs):
- super(AuthForm, self).__init__(*args, **kwargs)
- self.fields['username'].widget.attrs["size"] = 65
- self.fields['password'].widget.attrs["size"] = 65
- class PassResetForm(auth_forms.PasswordResetForm):
- def __init__(self, *args, **kwargs):
- super(PassResetForm, self).__init__(*args, **kwargs)
- self.fields['email'].widget.attrs["size"] = 65
- class EditProfileForm(ModelForm):
- date_birth = forms.DateField(('%d.%m.%Y',), label=_('Дата рождения'), required=False,
- widget = forms.DateInput(format='%d.%m.%Y', attrs={
- 'class':'input',
- 'size':'65'
- })
- )
- def __init__(self, *args, **kwargs):
- super(EditProfileForm, self).__init__(*args, **kwargs)
- self.fields['ICQ'].widget.attrs["size"] = 65
- self.fields['URL'].widget.attrs["size"] = 65
- self.fields['jabber'].widget.attrs["size"] = 65
- self.fields['mobile'].widget.attrs["size"] = 65
- self.fields['skype'].widget.attrs["size"] = 65
- self.fields['about'].widget.attrs["cols"] = 49
- self.fields['about'].widget.attrs["rows"] = 8
- class Meta:
- model = UserProfile
- fields = ['gender', 'date_birth', 'ICQ', 'URL', 'jabber', 'mobile', 'skype', 'about' ]
- #не работает http://code.djangoproject.com/ticket/13095
- #widgets = {
- # 'date_birth': forms.DateInput(format="%d.%m.%Y"),
- # }
Для проверки выключаем бекенд вывода писем в консоль, добавляем конфигурацию smtp сервера:
- EMAIL_HOST='smtp.server.ru'
- EMAIL_HOST_USER='musicmans.ru'
- EMAIL_HOST_PASSWORD='password'
- DEFAULT_FROM_EMAIL='musicmans.ru@server.ru'
- SERVER_EMAIL='musicmans.ru@server.ru'
Регистрируемся, выполняем команду django-mailer - manage.py send_mail. Проверяем почту.
Создадим crontab для сервера в develop и можно сразу прописать на сервере (отправка почты (раз в пять минут), повторная отправка (раз в двадцать минут), удаление неактивных пользователей (раз в сутки); будем добавлять вручную, ибо не так часто требуется):
- */5 * * * * vermus (/usr/bin/python /srv/musicmans/root/src/manage.py send_mail >> /srv/musicmans/logs/cron_mail.log 2>&1)
- 0,20,40 * * * * vermus (/usr/bin/python /srv/musicmans/root/src/manage.py retry_deferred >> /srv/musicmans/logs/cron_mail_deferred.log 2>&1)
- 0 0 * * * vermus (/usr/bin/python /srv/musicmans/root/src/manage.py cleanupregistration >> /srv/musicmans/logs/cleanupregistration .log 2>&1)
Запускаем тесты, и если все ок, переключаемся в trunk и мержим ветку users, закрываем все задачи в redmine и
$fab production deploy
(можно в него дописать запуск install.py)
Кстати в fabfile.py закрались ошибочки,
if "y" == prompt('Install the necessary applications (y/n)?', default="n"):
install_requirements();
надо выполнять после svn update, а svn update для production.py не будет обновлять maintenance_mode, так как для svn файл уже обновлен, также рестартовать необходимо и uwsgi, смотрим обновленный fabfile.py.
Ну и как обычно, результат смотрим http://musicmans.ru/.

Аутентификация через OpenId и написание тестов для нашего приложения в следующей статье.
ps. Как сделать подсвечивающиеся меню расскажу отдельным постом, если кто заинтересуется.