Показаны сообщения с ярлыком django. Показать все сообщения
Показаны сообщения с ярлыком django. Показать все сообщения

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

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

29 июня 2010 г.

musicmans.ru | Как сделать сайт на Django | Схема работы

Ну что же. Инструментарий у нас уже готов. Вникаем в общую схему работы.

Схема такая:
1. Разрабатываем локально, используя отладку Django в Eclipse (наверное будем использовать SQLite при разработке, чтобы было проще, плюс файл базы можно будет хранить в svn, для одного разработчика, я думаю, это нормально).
2. Subversion. Общепринятая структура svn проекта:

branches
tags
trunk

Как их сделать расскажу позже. Сейчас остановимся на теории.
Итак, trunk - рабочая копия проекта, trunk должен работать, не забываем про это.
Если trunk должен работать, то как коммитить недоделанные задачи? Для этого есть branches - ветки. Когда перед нами встает задача по модернизации или исправлению ошибок, создаем ветку (копию) из проекта trunk в branches.
После внесения изменений (и соответственных между ними коммитов/апдейтов ветки), выполняем слияние ветки с trunk.
tags - метки, это копии проекта в которые нельзя коммитить, обычно там хранятся копии релизов.

Если ничего не понятно, читаем книгу или ждем продолжения (рассмотрим вопрос, как это делается в Eclipse Subversive).

3. Используя Fabric, мы будем из tag/current с сервера с Subversion выкладывать релиз на сервер с сайтом, а также обновлять базу (с помощью South).

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

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

Технологии.

Серверная сторона - django. Конечно будем использовать сторонние django приложения, и не будем писать тесты, тестировать будут пользователи. :) Элементарные вещи о django рассматриваться не будут, для этого есть django book.

Клиентская сторона - наверное gwt. Пока не определился, но думаю внедрим.

Инструменты и техническая сторона

Техническая сторона - покупайте домен и vds. VDS можете купить два или сделать собственный сервер, например, дома, один для сайта, другой для хранения и управления кодом.
Собственно, предыдущие посты как раз были подготовкой к работе.
Система контроля версий - Subversion.
Управление проектом и баг-трекер - Redmine.
Среда разработки Eclipse (кстати вышел Helios 3.6) с pydev, Subversive + расширения по вкусу плюс второй экземпляр Eclipse для gwt.

Ну вот вроде бы и все. Если не все, допишу позже.
Сроки. Ориентируюсь на полгода до более-менее приличного сайта (потому что есть еще основная работа, к сожалению :) ).
Все этапы разработки сразу будут выкладываться на сайт http://musicmans.ru .

Следить за постами о разработке можно по тегу musicmans.ru.

15 февраля 2010 г.

SmartGwt + Django = REST

Этот пост будет просто рассуждением. Точнее даже, рассказом о личных выводах. Так вот, в прошлом я связывал django и gwt с помощью json-rpc. Это полезная вещь в целом, но очень трудоемкая для комплексного подхода к приложениям на SmartGWT, в котором уже есть класс DataSource, и более удобный вариант RestDataSource. В нем уже заложены такие возможности, как передача данных в форматах XML, JSON через HTTP, гибкая поддержка REST идеологии (GET, PUT, POST и DELETE), поддерживает пагинацию, поиск, сортировку и другое. Что это значит? Первое, используя REST - создается некая стандартизация запросов (собственно идеология REST), в отличии от JSON-RPC. Создавая REST поддержку на сервере, мы одновременно создаем API нашего сайта. Второе, нам не надо для каждого UI элемента прописывать свою функцию получения данных, создается один источник данных REST, который можно использовать повторно, также менять источники данных для элементов UI на лету. Все это очень здорово и практично.
Но json-rpc не стоит выкидывать на свалку. Некоторые вещи в которых не нужен громоздкий DataSource, я думаю, нужно делать с помощью него, например, нажатие на кнопку для голосования или какие-то атомарные операции.
Рассказывать как связать django-piston и RestDataSource не буду. Скажу лишь, что сделать это можно, и довольно просто.

27 декабря 2009 г.

GWT. Взаимодействие с Django

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

Я долго раздумывал, какой интерфейс взаимодействия выбрать. Технология-то одна - ajax, а вот какой инструмент более удобен? Можно попробовать использовать RequestBuilder, но этот метод не совсем удобен и не совсем универсален. Поэтому я выбрал json-rpc. Для него уже все создано, как на стороне django, так и на стороне GWT.

Json-rpc, это легкий протокол удаленного вызова процедур, использующий для своего функционирования формат Json.
Пример запроса и ответа:

--> { "method": "echo", "params": ["Hello JSON-RPC"], "id": 1}
<-- { "result": "Hello JSON-RPC", "error": null, "id": 1}

Начнем с django. Добавляем поддержку json-rpc в наш проект.

Импортируем django-json-rpc и добавляем точку входа в urls.py.
 

from jsonrpc import jsonrpc_site
import myproj.myapp.views # подключаем файл, в котором буду храниться rpc функции

urlpatterns += patterns('',
url(r'^json/', jsonrpc_site.dispatch, name="jsonrpc_mountpoint"),
)

Возьмем пример с readme:
 

from jsonrpc import jsonrpc_method

@jsonrpc_method('myapp.sayHello')
def whats_the_time(request, name='Lester'):
return "Hello %s" % name

Запускаем сервер, запускаем шел django
 

./manage.py runserver 8080
./manage.py shell

И тестируем:
 

>>> from jsonrpc.proxy import ServiceProxy
>>> s = ServiceProxy('http://localhost:8080/json/')
>>> s.myapp.sayHello('Sam')
{u'error': None, u'id': u'jsonrpc', u'result': u'Hello Sam'}

В общем, в документе все написано. Также в приложении есть своей браузер, зайти на него можно по адресу http://localhost:8080/json/browse/ , добавив в urls.py, например так:
 

if settings.DEBUG:
urlpatterns += patterns('',
url(r'^json/browse/', 'jsonrpc.views.browse', name="jsonrpc_browser"), # for the graphical browser/web console only, omissible
)

urlpatterns += patterns('',
url(r'^json/', jsonrpc_site.dispatch, name="jsonrpc_mountpoint"),
)


Я нашел несколько неточностей в документации, так, там name="jsonrpc_browser" без буквы "r" на конце, плюс url браузера
идет после r'^json/', который перекрывает его вызов.

Итак, rpc в django у нас теперь есть. Переходим к клиентской части rpc в gwt.

Проделываем все операции, указанные в readme и... у меня не заработало.
 

09:36:42.942 [ERROR] [myapp] Errors in 'jar:file:/lovely.gwt.jsonrpc-0.7.jar!/lovely/gwt/jsonrpc/client/JSONServiceBase.java'
09:36:42.957 [ERROR] [myapp] Line 29: The type JSONServiceBase must implement the inherited abstract method ServiceDefTarget.setRpcRequestBuilder(RpcRequestBuilder)


Судя по всему, в GWT 2.0 изменились какие-то механизмы в сервисах JSON. Наверное класс JSONServiceBase раньше не требовал реализации метода ServiceDefTarget.setRpcRequestBuilder.
Что ж, давайте взглянем на исходники.
Действительно, функции setRpcRequestBuilder нет.

Проект не обновляется, разбираться времени нет, так что пробуем еще один вариант gwt-json-rpc.
По инструкции подключаем библиотеку. В инструкции написано что библиотека использует свой JSON кодер/декодер, поэтому можно использовать простые типы java: String, int, boolean, Array, HashMap, ArrayList, Vector, вместо классов GWT. В общем, она даже проще, чем lovely-gwt-jsonrpc.
У меня получился следующий код.

 

//Create a new JsonRpc instance
JsonRpc jsonRpc = new JsonRpc();

//Create a callback handler
AsyncCallback callback = new AsyncCallback() {

public void onFailure(Throwable caught) {
SC.say(caught.toString());
};

public void onSuccess(Object result) {
SC.say(result.toString());
};
};
jsonRpc.request(
"http://localhost:8080/json/",
"myapp.sayHello",
null,
callback);

Что такое SC смотрим здесь.

После запуска мы обнаруживаем, что все результаты попыток вызова функции попадают в onFailure. Обусловлено это скорее всего тем, что сервер Django и сервер gwt находятся на разных портах, а это противоречит Same Origin Policy.
Далее, я скомпилировал проект, чтобы скопировать его в статический путь в проекте Django, и запустить под сервером Django, чтобы не нарушать SOP. И обнаружил, что CsrfMiddleware не дает скомпилированному gwt приложению делать POST запросы. Но мы знаем, что фрейморки, вроде jQuery позволяли это с легкостью делать. А все потому, что gwt-json-rpc при создании RequestBuilder не создает заголовок вида "X-Requested-With: XMLHttpRequest", поэтому django (точнее middleware) считает, что запрос сделан с другого домена, и возвращает ошибку 403. Значит надо сообщить ему об этом. AJAX запросы считаются безопасными и не нуждаются в проверке, так как современные браузеры придерживаются SOP. Придется добавить данный заголовок в код библиотеки и пересобрать ее. Разработчику проблему описал. Может быть на момент прочтения статьи в ней будет прописан данный заголовок (функция JsonRpc.request):

builder.setHeader("X-Requested-With", "XMLHttpRequest");

Я его добавил сам и сделал новый jar. Компилируем проект, копируем в путь для статики в django, и проверяем - все должно работать.
Остался один ньюанс - работа в дебаг режиме и ajax запросы. Не будем же мы каждый раз компилировать проект, тем более терять все прелести дебага. Воспользуемся HTTP proxy servlet.
Качаем сервлет, копируем в папку WEB-INF/lib, правим web.xml, у меня примерно следующее:


HttpProxy
com.jsos.httpproxy.HttpProxyServlet

host
http://localhost:8080/json/




HttpProxy
/json/


Меняем путь запроса:

jsonRpc.request(
"/json/",
"myapp.sayHello",
null,
callback);


и проверяем работу в дебаг режиме.

Вот и все. Удачной разработки.

22 декабря 2009 г.

GWT. Передача параметров в GWT

Этой серией буду учиться вместе с вами, как работать с GWT в виде фронтенда и Django в виде бекенда. Пишу простым языком, так как сам разбираюсь в процессе.

Для начала надо научиться передавать параметры в GWT. Первая и основная причина - идеология django - нельзя жестко использовать url адреса. Django для этого предоставляет множество средств, как то и специальный тег url в шаблонах, и специальную функцию reverse для использования в коде. То есть смысл определяется тем, что нельзя использовать url напрямую где бы-то ни было, кроме определения его в файле url.py. Любое изменение конфигурации url'ов в url.py не должно приводить к поломке приложения. В общем, для запуска GWT фронтенда нам необходим набор url-ов определенных в бекенде для ajax запросов.

На стороне Django мы используем тег {% url %}, и выводим url'ы ajax запросов с помощью него, с этим все понятно. Но как их передать в GWT?

Так как GWT и находится в клиентском окружении javascript окна, то мы можем передать параметры в темплейте Django как словарь javascript:



И получить эти url'ы в GWT:


public void useURLsDictionary() {
Dictionary URLsDict = Dictionary.getDictionary("ajaxsUrls");

String ajaxUrl1 = URLsDict.get("ajaxUrl1");
String ajaxUrl2 = URLsDict.get("ajaxUrl2");

}

27 октября 2009 г.

Дебагинг django в eclipse c помощью PyDev

Pydev в сентябре переехал. Да еще его расширения стали Open Source:
Pydev Extensions is now merged with Pydev, and its once closed source code has become open source. Thus, there is no more Pydev Extensions, only the open source Pydev, with all the capabilities of Pydev Extensions incorporated.


Изменился и сайт апдейта для eclipse - http://pydev.org/updates .

А что нам это дает? А это нам дает бесплатную штуку под названием "Debug 'server' for remote debugging", с помощью которой можно дебажить django очень удобным способом.

Для начала выполняем условия, как написано здесь, в том числе заносим в PYTHONPATH в настройках Eclipse путь примерно следующего содержания
"eclipse\plugins\org.python.pydev.debug_1.5.0.1251989166\pysrc\" (найдете у себя подобный) и запускаем сервер (из перспективы debug):



Добавляем следующий код в manage.py (после if __name__ == "__main__":) проекта, что дает нам возможность обернуть PyDev брейкпоинты в pydevd.settrace(), который пересылает трейс Debug Remote серверу (для теста рекомендую сначала использовать --noreload и pydevd.settrace(), и убедиться, что трейс возникает именно на Debug Remote server, так же можно попробовать запускать проект не в режиме дебага, а в режиме run, трейс должен отправляться в любом режиме):


import sys

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




А для чего мы это, собственно говоря, делали? А чтобы запускать runserver без --noreload. Чтобы и изменение кода и брейкпоинты обрабатывались "онлайн".
Примерно вот такая картина:

25 августа 2009 г.

Первый сайт на Django. izmenimsya.ru

Ну как бы больше для развлечения. :)



Что такое izmenimsya.ru?

Вы знаете, иногда мы хотим стать лучше, но все время это лучшее оттягиваем на потом.

Бросить курить, бросить выпивать, бросить наркотики, не изменять, жертвовать, не врать, делать добро, выучить физику.

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

Вот такие штуки можно добавлять в подпись на форумах или в блоги:




ps. Кстати, лого нарисован в blender :)

24 июня 2009 г.

Ldap авторизация в Django

Вопрос ldap авторизации в Python довольно хорошо рассмотрен. Теперь разберем, как привязать эту авторизацию к Django.
Первый вариант в рамках компании будет регистрация на сайте с django путем авторизации в ldap.
Другой вариант - полная интеграция авторизации ldap в джанго, но тогда все преимущества групп и контроля доступа теряются, если только не получать эту дополнительную информацию от ldap или других источников.
Третий, наиболее гибкий вариант. Отсутствие регистрации. Логиним пользователя - ищем его профиль в auth у Django - если его нет, создаем, если есть - то используем найденный профиль (если пароль в Django не совпадает с ldap паролем - обновляем его).

Давайте пойдем по третьему пути.

Для начала подготавливаем Django. У нас будет расширенный стандартный Django User, так как после авторизации пользователя в ldap мы еще получим его данные с корпоративной базы mssql.

Устанавливаем python-ldap:

$ sudo apt-get install python-ldap

Прописываем в settings.py:

LDAP_DOMAIN='mydomain.com'
LDAP_SERVER='ldaps://%s' % LDAP_DOMAIN #SSL connection

Правим файл auth.py:

# -*- coding: utf-8 -*-

from django.conf import settings
from django.contrib.auth.backends import ModelBackend
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_model
from social.models import CustomUser

class CustomUserModelBackend(ModelBackend):

def get_local_user(self, email, password=None):

try:
user = self.user_class.objects.get(email=email.lower())
if user.check_password(password):
return user
else:
self.exist_user=user#save user for refresh local data
except self.user_class.DoesNotExist:
return None

def get_ldap_refresh_create_user(self, username=None, password=None):
import ldap, sys

#create new local user
if '@' in username:
#user@domain
LDAP_USERNAME=username
username=username.split('@')[0]
else:
#user
LDAP_USERNAME='%s@%s' % (username, settings.LDAP_DOMAIN)

LDAP_PASSWORD=password

try:
# build a client
ldap_client = ldap.initialize(settings.LDAP_SERVER)
# perform a synchronous bind
ldap_client.simple_bind_s(LDAP_USERNAME, LDAP_PASSWORD)
except ldap.INVALID_CREDENTIALS, e:
return False
except ldap.SERVER_DOWN, e:
return False#@todo raise Validation error

#lpad auth succes:

#try to get local user
user=self.get_local_user(LDAP_USERNAME, password)

if user:#if we check user in localbase
return user
else:#else create new or refresh old pass
if hasattr(self, 'exist_user'):
#refresh local auth data from ldap in case of change pass
self.exist_user.set_password(password)
self.exist_user.save()#write new pass
user=self.exist_user
else:
user=CustomUser.objects.create_user(username,LDAP_USERNAME,password)

return user

def updape_user_info(self, user):
pass

def authenticate(self, username=None, password=None):
#try to auth with ldap
#and refresh user data in success or create new one if user does not exist
user=self.get_ldap_refresh_create_user(username, password)
if user:
self.updape_user_info(user)#get data from mssql
return user
else:
return None# error ldap auth

def get_user(self, user_id):
try:
return self.user_class.objects.get(pk=user_id)
except self.user_class.DoesNotExist:
return None

@property
def user_class(self):
if not hasattr(self, '_user_class'):
#self._user_class = get_model(*settings.CUSTOM_USER_MODEL.split('.', 3))
self._user_class = CustomUser
if not self._user_class:
raise ImproperlyConfigured('Could not get custom user model')
return self._user_class


Функция updape_user_info на самостоятельное написание, она должна дописывать необходимую расширенную информацию.

ps. прозрачная авторизация в принципе возможна, но только с IE, можете попробовать.