31 июля 2010 г.

musicmans.ru | Как сделать сайт на Django | Пользователи

Наконец-то мы добрались до самого сладкого. Начнем с приложения users. Мы помним, что в django, как и в python надо писать приложения, а не проекты, чтобы соблюдать принцип DRY. Это основное приложение, которое требуется почти в каждом проекте. Несмотря на то, что в django уже есть django.contrib.auth, класс models.User содержит только минимальный набор полей. Расширение полей этого класса существует в следующих вариантах.
Создадим и переключимся в ветку 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.*)$', 'django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),
)

а в общие настройки пропишем 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 %}
{{ field.label_tag }}{{ field }}{{ field.errors }}
{{ field.help_text }}

{% 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. Как сделать подсвечивающиеся меню расскажу отдельным постом, если кто заинтересуется.

19 июля 2010 г.

musicmans.ru | Как сделать сайт на Django | Развертывание

Я обещал выкладывать все этапы работы на http://musicmans.ru, поэтому настала пора вывесить табличку "Сайт в разработке" :), заодно наладив работу развертывания.

Итак, задачи: создать приложение по вводу сайта в режим обслуживания, настроить сервер, автоматизировать процесс развертки на сервер с помощью fabric.

Вспомним о том, что у нас есть redmine и mylyn, создадим данные задачи (не забываем создать категории задач в настройках проекта в redmine).

django-maintenancemode

Для ввода сайта в режим обслуживания есть целое приложение.

Устанавливаем:

C:\>c:\Python26\Scripts\pip.exe install django-maintenancemode
Downloading/unpacking django-maintenancemode
Downloading django-maintenancemode-0.9.2.tar.gz
Running setup.py egg_info for package django-maintenancemode
Installing collected packages: django-maintenancemode
Running setup.py install for django-maintenancemode
Successfully installed django-maintenancemode
Cleaning up...

Прописываем в requirements.txt:

django-maintenancemode==0.9.2

Настраиваем. В MIDDLEWARE_CLASSES добавляем "maintenancemode.middleware.MaintenanceModeMiddleware".



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

Функции приложения:
* MAINTENANCE_MODE - включает\выключает режим обслуживания, по умолчанию: False.
* Страница 503 не отображается залогиненым админам и клиентам с ip адресами, входящими в INTERNAL_IPS.

Итак, пропишем MAINTENANCE_MODE = True, в development.py и в production.py (в development.py закомментируем вскоре).

Запускаем pydev сервер, отладку, переходим на страницу и видим следующее:



Немного поправим 503.html по своему желанию.

Настройка сервера

Устанавливаем и настраиваем фаерволл:

$ sudo aptitude install ufw
$ sudo ufw enable
$ sudo ufw logging on
$ sudo ufw allow 80/tcp
$ sudo ufw allow SSH_port
$ sudo ufw default deny

Настройку веб сервера выбрал такую (nginx + uwsgi). Тем более, nginx, начиная с версии 0.8.40 поддерживает uwsgi из коробки.

# apt-get install gcc libssl-dev libpcre++-dev make
# wget http://sysoev.ru/nginx/nginx-0.8.44.tar.gz
# tar -xzvf nginx-0.8.44.tar.gz
# cd nginx-0.8.44/
# ./configure --conf-path=/etc/nginx/nginx.conf \
--prefix=/usr \
--error-log-path=/var/log/nginx/error.log \
--pid-path=/var/run/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--http-log-path=/var/log/nginx/access.log \
--with-http_dav_module \
--http-client-body-temp-path=/var/lib/nginx/body \
--with-http_ssl_module \
--http-proxy-temp-path=/var/lib/nginx/proxy \
--with-http_stub_status_module \
--http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
--http-uwsgi-temp-path=/var/lib/nginx/uwsgi \
--http-scgi-temp-path=/var/lib/nginx/scgi \
--with-debug \
--with-http_flv_module
# make
# make install

Скрипт запуска /etc/init.d/nginx я взял из стандартного пакета debian (устанавливать его не нужно, ибо можно перетереть новые конфиги старыми. В принципе, не страшно, так как мы будем их писать заново, но например mime.types могут отличаться).

Создаем рабочую директорию для сайта, например /srv/musicmans
Структура:

/srv/musicmans
| backups
--| src
--| db
| logs
| root
--| src
--| www

На машине разработчика создаем файл в src wsgi.py (основой файл запуска проекта для веб-сервера):


import os
import sys
import locale
import django.core.handlers.wsgi

DIR=(os.path.abspath(__file__))
sys.path.append(DIR)
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings.production'

def force_utf8_hack():
reload(sys)
sys.setdefaultencoding('utf-8')
for attr in dir(locale):
if attr[0:3] != 'LC_':
continue
aref = getattr(locale, attr)
locale.setlocale(aref, '')
(lang, enc) = locale.getlocale(aref)
if lang != None:
try:
locale.setlocale(aref, (lang, 'UTF-8'))
except:
os.environ[attr] = lang + '.UTF-8'

force_utf8_hack()


application = django.core.handlers.wsgi.WSGIHandler()

Перед тем, как настраивать сервер, запустим тестирование проекта.



Введем команду test и получим ошибку.

Добавим в development.py:

INSTALLED_APPS += (
'django.contrib.admin',
)

А также в manage.py:

if settings.DEBUG and command == "test":
settings.MAINTENANCE_MODE = False

execute_manager(settings)

Ибо нам тесты в режиме обслуживания не нужны, да и не отрабатывают они, у меня вышла ошибка отсутствия темплейта 503.html и куча других.

Сделаем коммит.

Вернемся к серверу с сайтом. Сделаем предварительную настройку:

1. Перейдем в директорию /srv/musicmans/ и заберем транк в root:

$export SVN_SSH="ssh -l loginname"

или сделаем пару ключа (она нам все рано пригодиться при использовании fabric). На сервере с сайтом:

$ ssh-keygen -t dsa
$ cat ~/.ssh/id_dsa.pub

копируем вывод, добавляем на сервер с кодом в ~/.ssh/authorized_keys2 на сервер (если файла нет, то touch ~/.ssh/authorized_keys2 && chmod 600 ~/.ssh/authorized_keys2 ). Пробуем логиниться без пароля.

$svn checkout --depth=empty svn+ssh://codesrv/repos/musicmans/trunk/backend root
$cd root/
$svn update --set-depth=infinity www
$svn update --set-depth=infinity src

Так как нам нужны только две директории src и www, то делаем пустой checkout, после чего обновляем две директории с бесконечной вложенностью. После этого svn update будет нам обновлять только директории www и src.

Устанавливаем необходимые приложения для сайта:

vermus@musicmans:~$ cd /srv/musicmans/root/src
vermus@musicmans:~$ sudo pip install -r requirements.txt --download-cache /usr/src/pipcache/

Установка postgresql:

# apt-get install postgresql python-psycopg2
# su postgres
$ createuser musicmans --no-superuser --no-createdb --no-createrole --login --pwprompt --encrypted
$ createdb --owner=musicmans --encoding=utf-8 musicmans

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

vermus@musicmans:~$ cd /srv/musicmans/root/src/
vermus@musicmans:/srv/musicmans/root/src$ python manage.py syncdb

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

Установим uwsgi сервер:

$ cd /usr/src/
$ sudo pip install http://projects.unbit.it/downloads/uwsgi-latest.tar.gz

Настроим скрипт init.d для запуска через файловый сокет сервера uwsgi с проектом (/etc/init.d/uwsgi):

# cat uwsgi
### BEGIN INIT INFO
# Provides: uwsgi
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts the uwsgi app server
# Description: starts uwsgi app server using start-stop-daemon
### END INIT INFO

PATH=/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/bin/uwsgi

OWNER=uwsgirun

NAME=uwsgi
DESC=uwsgi

test -x $DAEMON || exit 0

# Include uwsgi defaults if available
if [ -f /etc/uwsgi ] ; then
. /etc/uwsgi
fi

set -e

DAEMON_OPTS="--socket /var/lib/nginx/uwsgi/musicmans.sock --chmod-socket -d /srv/musicmans/logs/uwsgi.log --pythonpath $PYTHONPATH --module $MODULE"

case "$1" in
start)
echo -n "Starting $DESC: "
start-stop-daemon --start --chuid $OWNER:$OWNER --user $OWNER \
--exec $DAEMON -- $DAEMON_OPTS
echo "$NAME."
;;
stop)
echo -n "Stopping $DESC: "
start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \
--exec $DAEMON
echo "$NAME."
;;
reload)
killall -1 $DAEMON
;;
force-reload)
killall -15 $DAEMON
;;
restart)
echo -n "Restarting $DESC: "
start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \
--exec $DAEMON
sleep 1
start-stop-daemon --user $OWNER --start --quiet --chuid $OWNER:$OWNER \
--exec $DAEMON -- $DAEMON_OPTS
echo "$NAME."
;;
status)
killall -10 $DAEMON
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2
exit 1
;;
esac
exit 0

Не забываем создать пользователя uwsgirun, под которым будет запускаться uwsgi. Параметр chmod-socket устанавливает права 666 на сокет, если Вас это не устраивает смотрите документацию. Если uwsgi после запуска ругается на права, проверьте права на директорию с сокетом, на директорию с логами.
Создадим файл конфигурации /etc/uwsgi :

PYTHONPATH=/srv/musicmans/root/src
MODULE=wsgi

Обратите внимание, что мы указываем имя модуля python, а не имя файла.
Устанавливаем chmod 755 для скрипта /etc/init.d/uwsgi , загружаем при старте системы:

root@musicmans:/var/lib/nginx# chown -R uwsgirun uwsgi
root@musicmans:/etc/init.d# chmod 755 uwsgi
root@musicmans:/etc/init.d# update-rc.d -f uwsgi defaults
root@musicmans:/etc/init.d# /etc/init.d/uwsgi start

Конфиги nginx: nginx.conf, стандартный из пакета debian. Конфиг сайта:

root@musicmans:/etc/nginx/sites-available# cat musicmans
#serving Django.
upstream django {
ip_hash;
server unix:/var/lib/nginx/uwsgi/musicmans.sock;
}

server {
listen 80;
server_name musicmans.ru;
charset utf-8;
error_log /srv/musicmans/logs/nginx_error.log info;
access_log /srv/musicmans/logs/nginx_access.log;

# Django admin media.
#location /media/admin/ {
# alias lib/python2.6/site-packages/django/contrib/admin/media/;
# }

# Your project's static media.
location /media/ {
alias /srv/musicmans/root/www/media/;
}

# Finally, send all non-media requests to the Django server.
location / {
uwsgi_pass django;
include uwsgi_params;
}

location ~ /.svn/ {
deny all;
}

}

Включаем сайт

# ln -s /etc/nginx/sites-available/musicmans /etc/nginx/sites-enabled/musicmans

Перезапускаем /etc/init.d/uwsgi restart и /etc/init.d/nginx restart.

Заходим http://musicmans.ru/:



Процесс развертывания кода и структуры базы данных на сервер с помощью fabric

Установим на машину разработчика pip и fabric.

#pip install fabric

Создаем fab файл с командами fabric в корне проекта для установки необходимых приложений из requirements.txt, обновления кода, миграции базы данных и перезапуска Nginx:

* Включить режим обслуживания сайта (см. выше).
* Сделать резервную копию базы данных.
* Сделать резервную копию кода (src) сайта.
* Обновить код с репозитория subversion.
* Запустить миграцию базы данных (South).
* Выключить режим обслуживания сайта.

У нас fabric 0.9.1, а в 1.0 обещают поддержку django. Ну а пока ее нет создаем fabfile.py в корне проекта следующего содержания (перевод windows консоли для понимания удаленного UTF8 в случае ошибок - шрифт cmd окна Lucida Console (или любой другой true type), далее команда chcp 65001).


# -*- mode: python; coding: utf-8; -*-
import sys
from fabric.api import env, run, prompt, local, get, cd, sudo, require
from fabric.state import output
from fabric.contrib.files import uncomment
import datetime

now = datetime.datetime.now()

def production():
#здесь данные об удаленном сервере с сайтом
env.environment = "production"

env.hosts = ['codesrv']
env.user = 'vermus'
env.path = '/srv/musicmans/root'
env.root_path = '/srv/musicmans'

env.db_name = 'musicmans'
env.db_user = 'musicmans'

def deploy():
"""
In the current version fabfile no initial database creation and configure the virtual server host.
"""
require('environment', provided_by=[production])#дописать по желанию dev и stage

if env.environment == 'production':
if "y" != prompt('Are you sure you want to update the production site (test & check in trunk release code!)? (y/[n])?', default="n"):
return

if "y" == prompt('Set MAINTENANCE_MODE (y/n)?', default="y"):
maintenance_mode()

if "y" == prompt('Create database backup? (y/n)?', default="y"):
backup_db()

if "y" == prompt('Create source code backup? (y/n)?', default="y"):
backup_src()

update_from_svn()

if "y" == prompt('Install the necessary applications (y/n)?', default="n"):
install_requirements();

migrate_database()

maintenance_mode(set=False)

restart_webserver()

def install_requirements():
require('environment', provided_by=[production])#дописать по желанию dev и stage
print(" * install the necessary applications...")

requirements_file = env.path+'/src/requirements.txt'

args = ['install',
'-r', requirements_file,
'--download-cache', '/usr/src/pipcache/'
]

sudo('pip %s' % ' '.join(args))

def maintenance_mode(set=True):
require('environment', provided_by=[production])#дописать по желанию dev и stage
print(" * change production.py and restart nginx...")
if set:
uncomment(env.path+'/src/settings/production.py', 'MAINTENANCE_MODE = True')
else:
comment(env.path+'/src/settings/production.py', 'MAINTENANCE_MODE = True')

restart_webserver()

def backup_db():
require('environment', provided_by=[production])#дописать по желанию dev и stage
print(" * create database dump...")

db_name = env.db_name
db_user = env.db_user

backup_file = "backup_%d_%d_%d_%d_%d.sqlgzip" % (now.day, now.month, now.year, now.hour, now.minute)
backup_dir = env.root_path+'/backups/db'
with cd(backup_dir):
run("echo dbpassword | pg_dump -W -U %s -F c %s > %s" % (db_user, db_name, backup_file))

def backup_src():
require('environment', provided_by=[production])#дописать по желанию dev и stage
print(" * create source code backup...")
backup_dir = env.root_path+'/backups/src'
backup_file = "backup_%d_%d_%d_%d_%d.tar.gz" % (now.day, now.month, now.year, now.hour, now.minute)
src_dir = env.path+'/src'

run("mkdir -p %s" % backup_dir+'/all')
run("cp -f -R %s %s" % (src_dir, backup_dir+'/all'))
run("cp -f -R %s %s" % (env.path+'/www/static', backup_dir+'/all'))

with cd(backup_dir):
run ('tar -zcf %s %s' % (backup_file, backup_dir+'/all'))
run ('rm -f -R %s' % (backup_dir+'/all'))


def update_from_svn():
require('environment', provided_by=[production])#дописать по желанию dev и stage
with cd(env.path):
run('svn update') #svn checkout сделаем вручную первый раз

def migrate_database():
require('environment', provided_by=[production])#дописать по желанию dev и stage
with cd(env.path+'/src'):
run('python manage.py migrate --no-initial-data')
run('python manage.py syncdb')

def restart_webserver():
require('environment', provided_by=[production])#дописать по желанию dev и stage
print(" * restart nginx")
sudo('/etc/init.d/uwsgi restart', pty=True)
sudo('/etc/init.d/nginx force-reload', pty=True)

Схема такая:
Запуск $fab production deploy с машины разработчика - логин по ssh на сервер с сайтом (fabric выполняет автоматически при выполнении run, sudo и др., используя данные из env), далее выполняются необходимые действия, в том числе svn+ssh с сервера с кодом с транка.

Добавим в /etc/postgresql/8.4/main/pg_hba.conf следующую строчку:

# "local" is for Unix domain socket connections only
local musicmans musicmans md5
local all all ident

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

Статья получилась объемной, старался быстрее закончить с технической стороной и перейти наконец к созиданию. :)

Не забываем про задачи (перспектива planning), указываем время, потраченное на задачи и закрываем их.

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

4 июля 2010 г.

musicmans.ru | Как сделать сайт на Django | Миграция моделей и данных

В прошлый раз мы остановились на том, что запустили django на машине разработчика. Использование virtualenv оставим на самостоятельное рассмотрение. А вот pip мы все-таки установим. Тем более, что у нас в проекте уже лежит пустой файл requirements.txt - файл формата pip, со списком приложений, необходимых для нашего проекта. Кстати, пора в него уже прописать:
Django==1.2.1
South==0.7.2

Далее, качаем pip, разархивируем, устанавливаем:
#apt-get install python-setuptools
pip-0.7.2#python setup.py install

Теперь перейдем к теме.

South привносит в django возможность миграции структуры и данных модели. На практике это означает, что если мы что-то поменяли в модели (добавили/удалили поле) то south сам увидит изменения и создаст инструкции для внесения изменений в БД, которые останется только применить на всех экземплярах приложения.

Основные особенности, которые отмечают разработчики:

* отслеживание изменений в модели и создание миграций
* независимость от движков БД (заявлена поддержка 5 разных типов БД)
* создание миграций только для выбранного приложения (application)
* сообщение о возможных конфликтах при комите миграций от других разработчиков

Далее:

Устанавливаем South
>C:\Python26\Scripts\pip.exe install South
или
#pip install South

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

Добавляем south в INSTALLED_APPS и делаем syncdb (пробуем из gui).



Так как по умолчанию в INSTALLED_APPS прописано несколько стандартных приложений, в том числе django.contrib.auth, поэтому в консоли выполним создание суперпользователя сайта (первый запуск syncdb).

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

3 июля 2010 г.

musicmans.ru | Как сделать сайт на Django | Cтруктура проекта

Структура проекта может быть по вашему вкусу. Тут нет каких-то жестких правил, хотя правила в создании django приложений (не проекта) все-таки есть. Итак, приведем в пример структуру pinax сайта проекта:

apps
deploy
media
site_media
templates
tests

__init__.py
context_processors.py
manage.py
urls.py
settings.py

Мы подсмотрим у профессионалов, virtualenv мы использовать не будем, поэтому немного подсократим:

PROJECT_ROOT/
|-- apps/ # Django приложения сайта
|-- etcs/ # Разные конфигурационные файлы
|-- settings/ # Настройки
|-- templates/ # темплейты для всего сайта
|-- __init__.py # Инициализация пакета python
|-- requirements.txt #файл формата pip со списком приложений для сайта
|-- manage.py # файл запуска\управления проекта
`-- urls.py # Главный URLconf

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

|-- PROJECT_ROOT/
| |-- apps/
| |-- etc/
| |-- settings/
| | |-- __init__.py
| | |-- common.py
| | |-- development.py
| | `-- production.py
| |-- templates/
| |-- __init__.py
| |-- manage.py
| `-- urls.py
|-- www/
| |-- media/
| | |-- static/
|-- develop/

Статику (изображения, css, яваскрипт), которую веб сервер будет отдавать напрямую, мы вынесем за пределы PROJECT_ROOT.

Общие настройки определим в модуле settings.common. В development и production будут находиться специфичные настройки, а также они будут импортровать common, а manage.py будет загружать development.py, а в случае его отсутствия (svn:ignore) - production.py.

У меня получилась следующая структура проекта:



Обратите внимание на директорию develop, там будут лежать вещи, которые относятся к разработке, например отправим туда файл конфигурации development.py, вдруг кому-нибудь придется развертывать проект для разработки, тогда он легко сможет взять файл оттуда. Также обратите внимание на файл develompent.py в самом проекте, декоратор иконок subversive говорит нам о том, что он находится в svn:ignore.

Итак, переместим settings.py в common.py.

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

common.py:

# -*- mode: python; coding: utf-8; -*-
'''
Created on 02.07.2010
'''
import os
import sys

ADMINS = (
('Vermus', 'admin@musicmans.ru'),
)

MANAGERS = ADMINS

TIME_ZONE = 'Europe/Moscow'

LANGUAGE_CODE = 'ru-RU'

#Определяем корень проекта
PROJECT_ROOT = os.path.normpath(os.path.dirname(os.path.dirname(__file__)))

#Добавляем apps в системную переменную path
sys.path.insert(0, os.path.join(PROJECT_ROOT, 'apps'))

#URL к медиа файлам
MEDIA_URL = '/media/'
#путь в системе к медиа файлам
MEDIA_ROOT = os.path.join(os.path.dirname(PROJECT_ROOT), os.path.join('www', MEDIA_URL.strip('/') ))

#URL к статическим файлам
STATIC_URL = MEDIA_URL + 'static/'
#пусть в системе к статическим файлам
STATIC_ROOT = os.path.join(MEDIA_ROOT, 'static')

ROOT_URLCONF = 'urls'

#Админка нам не нужна, убираем
#ADMIN_MEDIA_PREFIX = '/media/'

TEMPLATE_DIRS = (
os.path.join(PROJECT_ROOT, 'templates'),
)

development.py (svn:ignore):

# -*- mode: python; coding: utf-8; -*-

from common import *

DEBUG = True
TEMPLATE_DEBUG = DEBUG

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

production.py:

# -*- mode: python; coding: utf-8; -*-

from common import *

DEBUG = False

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

Итак, единственное, что пока отличает production от develompent, переменная DEBUG и настройки базы данных (для нашего случая можно установить один пароль, и они тоже не будут отличаться, можно переместить в common.py). Остальное мы постарались сделать независимым от платформы и месторасположения проекта в системе.

Отключим в eclipse Window -> Preferences => Pydev -> Editor -> Code Analysis unused wild import, иначе будем получать много предупреждений от Code Analysis (в связи с нашими import *).

Отредактируем manage.py (в его начале мы разбираемся с settings проекта, потом настраиваем eclipse для отладки django проекта):

#!/usr/bin/env python
import sys

from django.core.management import execute_manager

try:
import settings.development as settings
except ImportError:
try:
import settings.production as settings
except:
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)

if __name__ == "__main__":
if len(sys.argv) > 1:
command = sys.argv[1]
if settings.DEBUG and (command == "runserver" or command == "testserver"):
# Make pydev debugger works for auto reload.
try:
import pydevd
except ImportError:
sys.stderr.write("Error: " +
"You must add org.python.pydev.debug.pysrc to your PYTHONPATH.")
sys.exit(1)

from django.utils import autoreload
m = autoreload.main
def main(main_func, args=None, kwargs=None):
import os
if os.environ.get("RUN_MAIN") == "true":
def pydevdDecorator(func):
def wrap(*args, **kws):
pydevd.settrace(suspend=False)
return func(*args, **kws)
return wrap
main_func = pydevdDecorator(main_func)

return m(main_func, args, kwargs)

autoreload.main = main

execute_manager(settings)

Выполняем все действия по вышеприведенной ссылке, чтобы запустить отладку проекта. Кстати, в новом pydev появился конфигуратор отладчика для django:



Но он многого не дает, как я понял, единственное --no-reload прописан по умолчанию и в аргументах можно просто указать runserver.

Итак, запускаем отладку:



И заходим браузером по адресу http://127.0.0.1:8000/, и видим классику: