6 ноября 2010 г.

musicmans.ru | Как сделать сайт на Django | GWT

Посмотрел я на дерево жанров и оно мне не понравилось. Страшное, неудобное. И решил сразу заняться клиентской стороной. Тем более у нас есть отличнейший повод!

Итак, настроим gwt. Скачиваем eclipse 3.6 для java.
Далле переходим на страницы с загрузками GWT. Ставим gwt плагин для eclipse.

Создаем проект File > New > Web Application Project.

Название: genre
package: ru.musicmans

Запуск - Debug As > Web Application.

Переходим по адресу, устанавливаем плагин. Все работает.

Устанавливаем GWT Designer. Читаем quick start.

Далее можно конвертировать, созданный проект из gwt plugin (gwt plugin мы поставили потому, что в нем все равно находиться сам gwt) в gwt java project, который идет с дизайнером (правой кнопкой на проекте - convert to) или создать новый:



Сразу создадим модуль ru.musicmans.genre.GenreTree:



Итак, проект создан. Открываем genre.java и кликаем на вкладку Design.



Что нам надо для организации передачи данных? Мы не можем использовать rpc call gwt, так как у нас на стороне сервера django. Что делать в данном случае? Я рассматривал данный вопрос около года назад. Итог такой: возможен вывод данных в темплейте и преобразование их в javascript object, но это не очень оптимальный путь, тем более, что приложению обычно нужны данные в процессе работы (в том числе, обновленные). Поэтому лучшим решением мне предоставляется REST (с помощью этого подхода не надо проходить новый путь создания интерфейсов сервисов). Я решил не использовать SmartGWT, слишком он навороченный. В чистом GWT нет поддержки REST, поэтому воспользуемся Restlet Framework, ну а со стороны django - django-piston.

Скачаем Restlet Framework, Edition for Google Web Toolkit. Установим (укажем java build path в свойствах проекта).

Django-Piston

Теперь перейдем к серверной стороне. django-piston я уже как-то упоминал. Так вот, устанавливаем:

>c:\Python26\Scripts\pip.exe install hg+http://bitbucket.org/jespern/django-piston@c4b2d21db51a#egg=piston

Читаем документацию. Создадим приложение api, пропишем (r'^api/', include('api.urls')), в главном urls.py. Создадим в приложении файлы urls.py и handlers.py. Остальные файлы, кроме __init__.py можно удалить.
handlers.py:

from django.core.urlresolvers import reverse

from piston.handler import BaseHandler#@UnresolvedImport

from genre.models import GenreDirStyle#@UnresolvedImport

class GenreHandler(BaseHandler):
allowed_methods = ('GET', )
fields = ('name', 'type', 'url' )
model = GenreDirStyle

@classmethod
def url(self, genre):
return reverse('genre_genre', args=[genre.id])

def read(self, request, genre_id):
genre = GenreDirStyle.objects.get(id=int(genre_id))
return genre

urls.py:

from django.conf.urls.defaults import *

from piston.resource import Resource#@UnresolvedImport

from api.handlers import GenreHandler#@UnresolvedImport

genre_resource = Resource(handler=GenreHandler)

urlpatterns = patterns('',
url(r'^genre/(?P[^/]+)/$', genre_resource, name='api_genre_id'),
)


Переходим по адресу, например http://localhost:8000/api/genre/3/ :

{
"url": "/genre/id/4/",
"type": 3,
"name": "Prog-Rock"
}

Чтобы открывать application/json в firefox, установите дополнение, а еще лучше используйте RESTClient для Firefox.

Вернемся к клиентской части.

Добавим widget дерево в gwt приложение tree:






Запускаем отладку, кстати, сразу рекомендую изменить параметр logLevel в конфигурации отладки.

С помощью firebug видим, что ответа на запрос по адресу http://localhost:8000/api/genre/3 не увенчались успехом, поэтому вспоминаем про проблему SOP.

В процессе поиска решений наткнулся на django-crossdomainxhr-middleware.py, позволяющее использовать кроссдоменные запросы (требуется firefox > 3.5).



Но мы пока его использовать не будем.

Итак, проблему SOP при разработке решим следующим образом. Отключим Jetty сервер, запускаемый в отладке, а также укажем порт 8000.


Сделаем символическую ссылку с директории war проекта на www\media\static\gwt\genre, под windows, например, так:

www\media\static\gwt>mklink /d genre d:\path\to\gwt\war\

Далее, можно отлаживать приложение по адресу примерно такому (предварительно запустив веб-сервер отладки django) http://localhost:8000/media/static/gwt/genre/GenreTree.html?gwt.codesvr=127.0.0.1:9997 адресу. Параметр gwt.codesvr обязателен при отладке gwt приложения.

А еще лучше создать темплейт django, подключив к нему приложение gwt примерно следующим образом. Создаем div с id='genreTreeEntryPointId' в темплейте, а в gwt root panel определяем следующим образом:
RootPanel rootPanel = RootPanel.get("genreTreeEntryPointId");

Теперь мы можем отлаживать gwt приложение прямо в "окружении" django-проекта, например, по адресу такому http://localhost:8000/genre/tree/?gwt.codesvr=127.0.0.1:9997&genre_id=2 .

Серверный и клиентский код выкладывать слишком много, остановимся на нюансах:

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

treeqs = GenreDirStyle.objects.raw("""SELECT t2.*
FROM genre_genredirstyle AS t1
LEFT JOIN genre_genredirstyle AS t2
ON t2.lft BETWEEN t1.lft AND t1.rgt
WHERE t1.lft < %s AND t1.rgt > %s AND t1.tree_id = 1 AND t2.depth-1 = t1.depth AND t2.tree_id = %s
ORDER BY t2.lft;""", (genre.lft, genre.rgt, genre.tree_id))

Построение дерева решил возложить на плечи клиентов.
- При загрузке приложения выводим div с изображением, который заменяется самим приложением, после его загрузки, а также выполнения всех ajax запросов.
- Настройки приложения выводим как JSON объект в javascript, получая значения которого в gwt приложении:

private final Dictionary paramsDict = Dictionary.getDictionary("gwtGenreParameters");
String paramsDict.get("API_GENRES_MAIN_URL");

- Автоматический разбор JSON ответа. Используем данное приложение.
Оно зависит от: google-gin и totoe (погуглите и подключайте в проект).
- Для обозначения состояния элемента дерева используем своей объект класса (сокращенный вид):

private class GenreTreeItemData
{
private int id;
private Boolean alreadyLoaded = false;
private String description;
}

используя функцию setUserObject(Object).
- При создании проекта, в настройках gwt приложения наследуется стиль по умолчанию gwt standart. Так вот, проблема в том, что в нем есть правила css, переопределяющие наши (в том числе body). Решить это можно двумя способами, вот первый, а можно просто удалить нежелательные строки из standard.css файла в директории gwt\standard\ (их там немного и они вначале).
- Для генерации документации по API используем вид from piston.doc import documentation_view.

После работы все как обычно и результат:



ps. На стороне сервера попробовал использовать Aptana 3.0, там действительно отменная поддержка Django темплейтов в PyDev (но наткнулся на баг, ctrl+space вешает IDE, может только у меня так?).

15 сентября 2010 г.

musicmans.ru | Как сделать сайт на Django | Жанры, направления, стили

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

Начнем с жанров, направлений и стилей. Создадим приложение genre, сразу создаем модель жанра.
Чтобы создать модель, нам надо определиться, что такое жанр собственно? К сожалению, в русском интернете, большая путница и мешанина из жанров, стилей и направлений. Нет ни исследований, ни достаточно устоявшихся критериев. Прочитав эту заметку начал задумываться структуре систематизации жанров и стилей. И главное, неплохо было бы найти уже существующую отлаженную структуру (Amazon). После долгих поисков (amg, amazon, mp3.com, discogs) остановился на варианте от amg.

Итак, напишем модель жанра, направления и стиля и заполним их структурой.

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

Код приводить не буду, расскажу что использовал lxml, а также окружение проекта для записи жанров/стилей.

После того как данные в базе, сделаем initial data для приложения:

>python manage.py dumpdata genre > apps\genre\fixtures\initial_data.json

Теперь попробуем вывести дерево жанров/стилей.

На данном этапе понимаем, что вывод "детей" стилей сулит нам лавинообразные запросы к базе данных (select_related нам тоже не поможет, он не срабатывает для моделей с полями отношения ForeignKey с null=True ( father = models.ForeignKey('self', verbose_name=_(u'Родитель'), related_name='child_dirs_styles', null=True) ) ), поэтому воспользуемся приложением для хранения деревьев в базе данных django-treebeard (документация).

C:\>c:\Python26\Scripts\pip.exe install git+git://github.com/tabo/django-treebeard@8eb52a4f4274615e86a7572a8bab39b79d718b88

Добавляем 'treebeard' в INSTALLED_APPS. Если вы используете админку, то настройка немного посложней.

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

Смотрим нашу модель:

# -*- coding:utf-8 -*-
from django.db import models
from django.utils.translation import ugettext_lazy as _
from treebeard.ns_tree import NS_Node #@UnresolvedImport

GENRE_DIR_STYLE = (
(0, _('Музыка')),
(1, _('Жанр')),
(2, _('Направление')),
(3, _('Стиль')),
)

class GenreDirStyle(NS_Node):
name = models.CharField(max_length=1000, verbose_name=_(u'Title'))
name_ru = models.CharField(max_length=1000, verbose_name=_(u'Название'), blank=True, null=True)
type = models.IntegerField(choices=GENRE_DIR_STYLE)
description = models.CharField(max_length=10000, verbose_name=_(u'Описание'), blank=True)

class Meta:
ordering = ["name"]
verbose_name = _(u'Жанр, направление, стиль')


Пробуем, работает ли миграции South с treebeard:

./manage.py schemamigration genre --auto

получаем сообщение следующего вида

? The field 'GenreDirStyle.lft' does not have a default specified, yet is NOT NULL.
? Since you are adding or removing this field, you MUST specify a default
? value to use for existing rows. Would you like to:
? 1. Quit now, and add a default to the field in models.py
? 2. Specify a one-off value to use for existing columns now
? Please select a choice:

South нас просит указать обязательное значение по умолчанию. Нажимаем 2, и значение 0.

Как создать дерево? Просто:
1. Создаем корень дерева.
pop_music = GenreDirStyle.add_root(name = "Популярная музыка", type = 0)#оно сразу сохраняется save()
2. Создаем жанр:
pop_music.add_child(name = "Rock", type = 1)

И здесь сталкиваемся с проблемой, в случае парсинга (см. выше) html и создания базы "на лету". Поэтому читаем basic-usage внимательней и переписываем примерно так:

>>> get = lambda node_id: Category.objects.get(pk=node_id)
>>> root = Category.add_root(name='Computer Hardware')
>>> node = get(root.id).add_child(name='Memory')
>>> get(node.id).add_sibling(name='Hard Drives')

>>> get(node.id).add_sibling(name='SSD')

>>> get(node.id).add_child(name='Desktop Memory')

>>> get(node.id).add_child(name='Laptop Memory')

>>> get(node.id).add_child(name='Server Memory')


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


# -*- coding: utf-8 -*-
from annoying.decorators import render_to#@UnresolvedImport
from django.shortcuts import get_object_or_404

from models import GenreDirStyle

@render_to('genres/genre_tree.html')
def genre_tree(request):

pop_genre = GenreDirStyle.objects.get(name="Популярная музыка", type=0)
classic_genre = GenreDirStyle.objects.get(name="Классическая музыка", type=0)

pop_tree = GenreDirStyle.get_tree(pop_genre)
classic_tree = GenreDirStyle.get_tree(classic_genre)

return {
'pop_tree': pop_tree,
'classic_tree': classic_tree
}

@render_to('genres/genre_genre.html')
def genre_genre(request, genre_id):

genre = get_object_or_404(GenreDirStyle, id = int(genre_id))

return {
'genre': genre
}

На данном этапе django-toolbar показывал замечательные 5 запросов за 13 мс. А вот общая генерация страницы занимала 6573.00 ms. Это очень долго, хотя при выключенном debug режиме ощутимо быстрее. Все упирается в рендеринг. Проэтому применим кеш в темплейте (на шесть часов, например):

{% load cache %}
{% cache 21600 pop_tree_chache %}
{% for node in pop_tree %}
{% include "genres/genre_node.html" %}
{% endfor %}
{% endcache %}

А также включим на время (позже настроим memcached на сервере) кеширование в память, в settings/common.py:

CACHE_BACKEND = 'locmem:///'

Темплейты интуитивно понятны, покажу лишь темплейт жанра, включаемый в цикл вывода дерева.

{% load dj_tags %}


(Ужасно, blogger все сломал, смотрите здесь)
Обратите внимание на фильтр multiply и substract. Это не стандартные фильтры django, а написаные в нашем приложении dj_tags.

# -*- coding: utf-8 -*-
from django import template
register=template.Library()

@register.filter(name='multiply')
def multiply(value, arg):
return int(value) * int(arg)

@register.filter(name='subtract')
def subtract(value, arg):
return int(value) - int(arg)

Итак, мы познакомились с хранением деревьев в базе данных с django, их выводом, затронули кеширование, написали пару темплейт тегов.

Ну, окончание, как обычно, тесты, мерж, развертывание.




ps. Не забудем обновить django (>c:\Python26\Scripts\pip.exe install --upgrade Django и прописать в requirements.txt)!

7 августа 2010 г.

musicmans.ru | Как сделать сайт на Django | Пользователи. Личные сообщения. Уведомления

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

#pip install git+git://github.com/robhudson/django-debug-toolbar
Смело ставим последнюю версию, ибо если что-то сломается, то на машине разработчика это не критично.
development.py:

INSTALLED_APPS += (
'django.contrib.admin',#tests
'debug_toolbar',
)

MIDDLEWARE_CLASSES += (
'debug_toolbar.middleware.DebugToolbarMiddleware',
)

INTERNAL_IPS = ('127.0.0.1',)


Обновляем localhost:


Поинтересуйтесь содержимым пунктов меню, там много интересного.

Итак, личные сообщения. Опять все написано за нас. Устанавливаем из транка (кстати, вот консольный svn клиент для windows):

>pip.exe install svn+http://django-messages.googlecode.com/svn/trunk/@141#egg=django_messages

141-я ревизия транка на данный момент, как написано на странице проекта это будущая версия 0.5, совместимая с Django 1.2 (как раз то, что нам нужно). Не забываем requirements.txt (кстати, я подумал, что неплохо было бы сначала добавлять строчку в requirements.txt, а ставить основываясь на файле. Надо только использовать одну директорию с кешом pip, чтобы он постоянно не скачивал дистрибутивы. Ставить установленные приложения по новой он не будет, но мы будем уверены, что не забудем прописать все приложения в requirements.txt и не испытаем проблем на сервере с сайтом).

Документация по настройке. Тут кстати возникает путаница, здесь приложение указано как django_messages, а в документации используется messages, так что используем django_messages.

Изменим темплейты.

Скопируем из приложения и изменим под свои нужды.



В документации к приложению читаем, что оно использует django-mailer и django-notification, если они установлены. Первое у нас есть, ставим второе (хорошее приложение, пригодится в будущем).
>pip.exe install git+git://github.com/jtauber/django-notification@3f023adf0ce2eafcee744904e2c358792f253721@egg=notification

Пропишем 'django_messages', 'notification' в приложения, настроим url.py. Синхронизируем базу.

Просмотрим таблицу notification_noticetype, в ней должны находиться оповещения о работе с личными сообщениями. Приложение notification рассмотрим ниже.

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

>pip.exe install git+git://github.com/ericflo/django-pagination@47e7ec874cd7dddda5ed13ffb6993a64dced2537

Настраиваем. Добавляем css разметку.

А также переведем пару строк, так как в приложении нет русской локализации, djutils\locale\ru_RU\LC_MESSAGES\django.po:
msgid "previous"
msgstr "назад"

msgid "next"
msgstr "вперед"

Не забудем скомпилировать.

в темплейтах с сообщениями добавляем (с месторасположением самостоятельно, 2-ка для теста):

{% load pagination_tags %}
{% autopaginate message_list 2 %}
{% paginate %}


В итоге получается:


Чтобы пользователи не были у нас безликими, давайте организуем им вывод профайлов.
В user/url.conf

url(r'^profile/(?P\d+)/$', users_views.userprofile, name='users_profile'),

(блогспот пытается закрыть тег /userprofile_id самостоятельно, конечно это в коде не требуется)
Вид:

@login_required
@render_to('users/user_profile.html')
def userprofile(request, userprofile_id):

request_user = get_object_or_404(User, id=int(userprofile_id), is_active=True)

return {
'request_user' : request_user,
}

Отмечу, что получать надо user, а не наш UserProfile, который еще может быть не создан.

Соответственно, напишем темплейт.



django_messages\templates\notification\ переведите вручную, перевод, который идет с приложением не работает (устарел наверное). Скопируем темплейты в users/templates, создадим папку locale в приложении и запустим создание файла перевода:
>python C:\Python26\Lib\site-packages\django\bin\django-admin.py makemessages -e .html,.txt --locale=ru_RU
в директории users.
После редактирования скомпилируем.

Темлейт для уведомлений notices.html для самостоятельного написания (можно подсмотреть в pinax).

Что такое notification? Это приложение для уведомлений. Когда происходит событие в системе, мы имеем возможность создать уведомление для пользователя, с возможностью настройки дополнительных рассылок уведомлений:

* при логине на сайт.
* по почте (настраивается пользователем).
* по rss

Подключим контекстный процессор "notification.context_processors.notification".

Мне не понравилась часть приложения по работе с url. Поэтому скопировал приложение себе в проект и поправил под свои нужды, получилось примерно следующее:



Пишите сообщения. :)

3 августа 2010 г.

musicmans.ru | Как сделать сайт на Django | Пользователи. Дополнительная аутентификация

В прошлый раз мы остановились на том, что начали создавать приложение users. Давайте создадим openid аутентификацию на сайте.
Я обещал писать бекенды для django-registration, но оказалось существует замечательное приложение для всевозможных видов аутентификации с последующим созданием пользователя и авторизации.

* OpenID - yandex.ru, rambler.ru, yahoo.ru, google.com
* OAuth - twitter.com
* OpenAPI - Вконтакте.ру
* FacebookConnect - facebook.com

Устанавливаем:
c:\Python26\Scripts\pip.exe install hg+http://bitbucket.org/offline/django-p
ublicauth@7371e8f71be1#egg=django-publicauth

Прописываем в requirements.txt.
Добавляем 'publicauth' в INSTALLED_APPS. Добавляем 'annoying.middlewares.RedirectMiddleware' в MIDDLEWARE_CLASSES.

Добавим 'publicauth.PublicBackend' в AUTHENTICATION_BACKENDS.

Запускаем syncdb.

OpenID

Необходимо установить python-openid (2.2.5).

Добавим
(r'', include('publicauth.urls')),
в файл /users/urls.py , тем самым переопределив url login и logout приложения нашими url'ами, и оставив остальные, необходимые для работы приложения. Ознакомимся с содержимым темплейта login.html, и добавим следующую форму в наш login.html:


Openid URL




Создадим директорию publicauth в users/templates/ , скопируем туда содержимое и поправим под наш сайт.
Также в файле users/forms.py создадим форму ExtraForm (форма для заполнения дополнительных полей после сторонней аутентификации, например имя пользователя).
Подсмотреть можно здесь.

PUBLICAUTH_EXTRA_FORM = "users.forms.ExtraForm"

Пробуем логиниться.

Приложение требует установленного и настроенного messages framework, как указано в документации (для самостоятельного рассмотрения) для вывода сообщений. У меня уже оно почти настроено (сообщение при сохранении профиля реализованно как раз через него).

Сообщения к сожалению не переведены, придется сделать это самим.

Создадим файл src/djutils/locale/ru_RU/LC_MESSAGES/django.po и переведем файл.

msgid "To complete registration, check your email and activate your account"
msgstr "Для завершения регистрации проверьте e-mail и активируйте учетную запись"

msgid "We are sorry, but registration is disabled. Come back later"
msgstr "Извините, но регистрация закрыта"

msgid "Please fill openid url field"
msgstr "Пожалуйста, заполните поле openid"

msgid "Your authentication provider returned bad response, please try again"
msgstr "Ваш провайдер аутентификации вернул влохой ответ, попробуйте еще раз"

msgid "You have cancelled OpenID authentication"
msgstr "Вы отменили аутентификацию по OpenID"

msgid "OpenID authentication failed. Reason: %s"
msgstr "Аутентификация OpenID провалилась. Причина: %s"

msgid "You have successfully logged out"
msgstr "Вы успешно вышли"

msgid "Your existing account was merged with new authentication account"
msgstr "Существующая учетная запись была объединена с новой учетной записью"

msgid "Your account is not activated. Please activate it first."
msgstr "Ваша учетная запись не активирована. Пожалуйста, активируйте ее сначала."

msgid "You have successfully authenticated"
msgstr "Вы успешно аутентифицированны"

msgid "Invalid response received from facebook server, please start the authentication process again"
msgstr "Неверный ответ от сервера facebook, пожалуйста запустите процесс аутентификации еще раз"

msgid "Invalid response received from OpenID server, please start the authentication process again"
msgstr "Неверный ответ от сервера OpenID, пожалуйста запустите процесс аутентификации еще раз"

msgid "Invalid response received from vkontakte server, please start the authentication process again"
msgstr "Неверный ответ от сервера vkontakte, пожалуйста запустите процесс аутентификации еще раз"

Скомпилируем (запуск в директории src\apps\djutils\)
>python C:\Python26\Lib\site-packages\django\bin\django-admin.py compilemessages.

Добавим 'django.middleware.locale.LocaleMiddleware', в MIDDLEWARE_CLASSES.

ВКонтакте

Делаем все как по ссылке на хабр. Единственно, я не понял для чего нужно VKONTAKTE_API_KEY, работает и без него, да и в бекенде vkontakte.py он не используется.

Facebook

Делаем по мануалу, ссылка на документацию на facebook.

В настройки надо добавить
FACEBOOK_PROFILE_MAPPING={ 'name': 'username', }

OAuth (Twitter)

Install python-oauth:
>pip.exe install oauth

Настройки:

TWITTER_CONSUMER_KEY = "key"
TWITTER_CONSUMER_SECRET = "secret"
TWITTER_REQUEST_TOKEN_URL = "https://twitter.com/oauth/request_token"
TWITTER_ACCESS_TOKEN_URL = "https://twitter.com/oauth/access_token"
TWITTER_AUTHORIZE_URL = "https://twitter.com/oauth/authorize"
TWITTER_API_URL = "http://twitter.com/users/show.json?user_id=%s"
TWITTER_PROFILE_MAPPING = { 'screen_name': 'username', }

Темплейт:




В итоге, в url auth_login мы должны иметь авторизацию на сайте или аутентификацию в сторонних сайтах в случае неавторизированного пользователя, а также аутентификацию в сторонних сайтах и сопоставления этих профилей с текущим профилем. И, естественно, логин любым из методов для существующего пользователя.

Как обычно, запускаем тесты, мержим транк, делаем развертывание, и сравниваем musicmans.ru.



ps. По поводу тестов. Да все уже написано:
http://djangotesting.com/
http://habrahabr.ru/blogs/django/91471/
http://pyobject.ru/blog/2009/09/13/django-external-test-tools/
http://night-fairy-tales.com/2009/10/django-eclipse.html

Разбираемся, пишем (На данный момент нами написано лишь редактирование профиля в приложении users, вот для него и можно написать тесты).

pps. Насткнулся на баг в тестах django-registration.
Вот решение (developmet.py)

TEST = False
manage_command = filter(lambda x: x.find('manage.py') != -1, sys.argv)
if len(manage_command) != 0:
command = sys.argv.index(manage_command[0]) + 1
if command < len(sys.argv):
TEST = sys.argv[command] == "test"

if TEST:
LANGUAGE_CODE = 'en-us'

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/, и видим классику: