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:
  1. from django.core.urlresolvers import reverse  
  2.   
  3. from piston.handler import BaseHandler#@UnresolvedImport  
  4.   
  5. from genre.models import GenreDirStyle#@UnresolvedImport  
  6.   
  7. class GenreHandler(BaseHandler):  
  8.     allowed_methods = ('GET', )  
  9.     fields = ('name''type''url' )  
  10.     model = GenreDirStyle  
  11.  
  12.     @classmethod  
  13.     def url(self, genre):  
  14.         return reverse('genre_genre', args=[genre.id])  
  15.   
  16.     def read(self, request, genre_id):  
  17.         genre = GenreDirStyle.objects.get(id=int(genre_id))  
  18.         return genre  

urls.py:
  1. from django.conf.urls.defaults import *  
  2.   
  3. from piston.resource import Resource#@UnresolvedImport  
  4.   
  5. from api.handlers import GenreHandler#@UnresolvedImport  
  6.   
  7. genre_resource = Resource(handler=GenreHandler)  
  8.   
  9. urlpatterns = patterns('',  
  10.                         url(r'^genre/(?P<genre_id>[^/]+)/$', genre_resource, name='api_genre_id'),   
  11.                         )  
  12. </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 .

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

- Выбор конкретного жанра в дереве (например при нажатии на меню жанров сверху). В этом случае загрузка списка для создания дерева вложенного множества осуществляется следующим образом (нам надо загрузить ветку, каждый элемент которой должен загрузить соседей):
  1. treeqs = GenreDirStyle.objects.raw("""SELECT t2.* 
  2.             FROM genre_genredirstyle AS t1 
  3.             LEFT JOIN genre_genredirstyle AS t2 
  4.             ON t2.lft BETWEEN t1.lft AND t1.rgt 
  5.             WHERE t1.lft < %s AND t1.rgt > %s AND t1.tree_id = 1 AND t2.depth-1 = t1.depth AND t2.tree_id = %s 
  6.             ORDER BY t2.lft;""", (genre.lft, genre.rgt, genre.tree_id))  

Построение дерева решил возложить на плечи клиентов.
- При загрузке приложения выводим div с изображением, который заменяется самим приложением, после его загрузки, а также выполнения всех ajax запросов.
- Настройки приложения выводим как JSON объект в javascript, получая значения которого в gwt приложении:
  1. private final Dictionary paramsDict = Dictionary.getDictionary("gwtGenreParameters");  
  2. String paramsDict.get("API_GENRES_MAIN_URL");  

- Автоматический разбор JSON ответа. Используем данное приложение.
Оно зависит от: google-gin и totoe (погуглите и подключайте в проект).
- Для обозначения состояния элемента дерева используем своей объект класса (сокращенный вид):
  1. private class GenreTreeItemData  
  2. {  
  3.  private int id;  
  4.  private Boolean alreadyLoaded  = false;  
  5.  private String description;  
  6.  }  

используя функцию 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 базе. Нам лишь стоит воспользоваться набором функций.

Смотрим нашу модель:
  1. # -*- coding:utf-8 -*-  
  2. from django.db import models  
  3. from django.utils.translation import ugettext_lazy as _  
  4. from treebeard.ns_tree import NS_Node #@UnresolvedImport   
  5.   
  6. GENRE_DIR_STYLE = (  
  7.     (0, _('Музыка')),  
  8.     (1, _('Жанр')),  
  9.     (2, _('Направление')),  
  10.     (3, _('Стиль')),  
  11. )  
  12.   
  13. class GenreDirStyle(NS_Node):  
  14.     name = models.CharField(max_length=1000, verbose_name=_(u'Title'))  
  15.     name_ru = models.CharField(max_length=1000, verbose_name=_(u'Название'), blank=True, null=True)  
  16.     type = models.IntegerField(choices=GENRE_DIR_STYLE)  
  17.     description = models.CharField(max_length=10000, verbose_name=_(u'Описание'), blank=True)  
  18.   
  19.     class Meta:  
  20.         ordering = ["name"]  
  21.         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 внимательней и переписываем примерно так:
  1. >>> get = lambda node_id: Category.objects.get(pk=node_id)  
  2. >>> root = Category.add_root(name='Computer Hardware')  
  3. >>> node = get(root.id).add_child(name='Memory')  
  4. >>> get(node.id).add_sibling(name='Hard Drives')  
  5. <category: category:="" hard="" drives="">  
  6. >>> get(node.id).add_sibling(name='SSD')  
  7. <category: category:="" ssd="">  
  8. >>> get(node.id).add_child(name='Desktop Memory')  
  9. <category: category:="" desktop="" memory="">  
  10. >>> get(node.id).add_child(name='Laptop Memory')  
  11. <category: category:="" laptop="" memory="">  
  12. >>> get(node.id).add_child(name='Server Memory')  
  13. <category: category:="" server="" memory="">  
  14. </category:></category:></category:></category:></category:>  

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

  1. # -*- coding: utf-8 -*-  
  2. from annoying.decorators import render_to#@UnresolvedImport  
  3. from django.shortcuts import get_object_or_404  
  4.   
  5. from models import GenreDirStyle  
  6.  
  7. @render_to('genres/genre_tree.html')  
  8. def genre_tree(request):  
  9.   
  10.     pop_genre = GenreDirStyle.objects.get(name="Популярная музыка", type=0)  
  11.     classic_genre = GenreDirStyle.objects.get(name="Классическая музыка", type=0)  
  12.   
  13.     pop_tree = GenreDirStyle.get_tree(pop_genre)  
  14.     classic_tree = GenreDirStyle.get_tree(classic_genre)  
  15.      
  16.     return {  
  17.           'pop_tree': pop_tree,  
  18.           'classic_tree': classic_tree  
  19.           }   
  20.  
  21. @render_to('genres/genre_genre.html')  
  22. def genre_genre(request, genre_id):  
  23.       
  24.     genre = get_object_or_404(GenreDirStyle, id = int(genre_id))  
  25.   
  26.     return {  
  27.             'genre': genre  
  28.           }   

На данном этапе django-toolbar показывал замечательные 5 запросов за 13 мс. А вот общая генерация страницы занимала 6573.00 ms. Это очень долго, хотя при выключенном debug режиме ощутимо быстрее. Все упирается в рендеринг. Проэтому применим кеш в темплейте (на шесть часов, например):
  1. {% load cache %}  
  2. {% cache 21600 pop_tree_chache %}  
  3. {% for node in pop_tree %}  
  4.   {% include "genres/genre_node.html" %}  
  5. {% endfor %}  
  6. {% endcache %}  

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

CACHE_BACKEND = 'locmem:///'

Темплейты интуитивно понятны, покажу лишь темплейт жанра, включаемый в цикл вывода дерева.
  1. {% load dj_tags %}  
  2. <div style="padding-left:{{ node.get_depth|multiply:20|subtract:20 }}px;">  
  3. <h{{ node.type|add:"1"="" }}="">  
  4. <a href="{% url genre_genre node.pk %}">{% if node.get_depth > 1 %}{{ node.get_type_display }} {% endif %}{{ node.name }}</a>  
  5. </h{{>  
  6. </div>  

(Ужасно, blogger все сломал, смотрите здесь)
Обратите внимание на фильтр multiply и substract. Это не стандартные фильтры django, а написаные в нашем приложении dj_tags.
  1. # -*- coding: utf-8 -*-  
  2. from django import template  
  3. register=template.Library()  
  4.  
  5. @register.filter(name='multiply')  
  6. def multiply(value, arg):  
  7.     return int(value) * int(arg)  
  8.  
  9. @register.filter(name='subtract')  
  10. def subtract(value, arg):  
  11.     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:
  1. INSTALLED_APPS += (  
  2.                    'django.contrib.admin',#tests  
  3.                    'debug_toolbar',  
  4.                    )  
  5.   
  6. MIDDLEWARE_CLASSES += (      
  7.                        'debug_toolbar.middleware.DebugToolbarMiddleware',  
  8.                        )  
  9.   
  10. 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-ка для теста):
  1. {% load pagination_tags %}  
  2. {% autopaginate message_list 2 %}  
  3. {% paginate %}  


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


Чтобы пользователи не были у нас безликими, давайте организуем им вывод профайлов.
В user/url.conf
  1. url(r'^profile/(?P<userprofile_id>\d+)/$', users_views.userprofile, name='users_profile'),  
  2. </userprofile_id>  

(блогспот пытается закрыть тег /userprofile_id самостоятельно, конечно это в коде не требуется)
Вид:
  1. @login_required  
  2. @render_to('users/user_profile.html')  
  3. def userprofile(request, userprofile_id):  
  4.    
  5.     request_user = get_object_or_404(User, id=int(userprofile_id), is_active=True)  
  6.       
  7.     return {  
  8.           'request_user' : request_user,  
  9.           }    

Отмечу, что получать надо 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:
  1. <form action="{% url publicauth-begin 'openid' %}?next={{ request.GET.next }}" method="post" id="openid_login">  
  2.     Openid URL  
  3.     <input type="text" name="openid_url">  
  4.     <p><input type="submit" value="Continue"></p>  
  5. </form>  

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

PUBLICAUTH_EXTRA_FORM = "users.forms.ExtraForm"

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

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

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

Создадим файл src/djutils/locale/ru_RU/LC_MESSAGES/django.po и переведем файл.
  1. msgid "To complete registration, check your email and activate your account"  
  2. msgstr "Для завершения регистрации проверьте e-mail и активируйте учетную запись"  
  3.   
  4. msgid "We are sorry, but registration is disabled. Come back later"  
  5. msgstr "Извините, но регистрация закрыта"  
  6.   
  7. msgid "Please fill openid url field"  
  8. msgstr "Пожалуйста, заполните поле openid"  
  9.   
  10. msgid "Your authentication provider returned bad response, please try again"  
  11. msgstr "Ваш провайдер аутентификации вернул влохой ответ, попробуйте еще раз"  
  12.   
  13. msgid "You have cancelled OpenID authentication"  
  14. msgstr "Вы отменили аутентификацию по OpenID"  
  15.   
  16. msgid "OpenID authentication failed. Reason: %s"  
  17. msgstr "Аутентификация OpenID провалилась. Причина: %s"  
  18.   
  19. msgid "You have successfully logged out"  
  20. msgstr "Вы успешно вышли"  
  21.   
  22. msgid "Your existing account was merged with new authentication account"  
  23. msgstr "Существующая учетная запись была объединена с новой учетной записью"  
  24.   
  25. msgid "Your account is not activated. Please activate it first."  
  26. msgstr "Ваша учетная запись не активирована. Пожалуйста, активируйте ее сначала."  
  27.   
  28. msgid "You have successfully authenticated"  
  29. msgstr "Вы успешно аутентифицированны"  
  30.   
  31. msgid "Invalid response received from facebook server, please start the authentication process again"  
  32. msgstr "Неверный ответ от сервера facebook, пожалуйста запустите процесс аутентификации еще раз"  
  33.   
  34. msgid "Invalid response received from OpenID server, please start the authentication process again"  
  35. msgstr "Неверный ответ от сервера OpenID, пожалуйста запустите процесс аутентификации еще раз"  
  36.   
  37. msgid "Invalid response received from vkontakte server, please start the authentication process again"  
  38. 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', }

Темплейт:
  1. <a href="{% url publicauth-begin 'twitter' %}?next={{ request.GET.next }}" id="twitter"><img title="{% trans " войти=""></a>  


В итоге, в 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)
  1. TEST = False  
  2. manage_command = filter(lambda x: x.find('manage.py') != -1, sys.argv)  
  3. if len(manage_command) != 0:  
  4.     command = sys.argv.index(manage_command[0]) + 1  
  5.     if command < len(sys.argv):  
  6.         TEST = sys.argv[command] == "test"  
  7.   
  8. if TEST:  
  9.     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:
  1. # -*- mode: python; coding: utf-8; -*-  
  2. from django.core.management import setup_environ  
  3. try:  
  4.     import settings.development as settings  
  5. except ImportError:  
  6.     import settings.production as settings  
  7. setup_environ(settings)  
  8.   
  9. from django.contrib.sites.models import Site  
  10. s = Site.objects.get(pk=1)  
  11. s.domain = "musicmans.ru"  
  12. s.name = "Меломаны"  
  13. 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, следующим содержимым:
  1. from django.contrib.sites.models import Site, RequestSite  
  2.   
  3. def current_site(request):  
  4.     try:  
  5.         current_site = Site.objects.get_current()  
  6.     except Site.DoesNotExist:  
  7.         current_site = RequestSite(request)  
  8.       
  9.     return {  
  10.             'SITE_NAME': current_site.name,  
  11.             'SITE_DOMAIN': current_site.domain,  
  12.             }  
  13.       
  14. def settings_processor(*settings_list):  
  15.     def _processor(request):  
  16.         from django.conf import settings  
  17.         settings_dict = {}  
  18.         for setting_name in settings_list:  
  19.             settings_dict[setting_name] = getattr(settings, setting_name)  
  20.         return settings_dict  
  21.     return _processor  
  22.   
  23. dj_settings = settings_processor(  
  24.    'STATIC_URL''DEBUG'  
  25. )  

(Не пугайтесь, Site.objects.get_current() кешируется)
В /settings/common.py добавим:
  1. TEMPLATE_CONTEXT_PROCESSORS = (  
  2.                                "django.contrib.auth.context_processors.auth",  
  3.                                "django.core.context_processors.debug",  
  4.                                "django.core.context_processors.i18n",  
  5.                                "django.core.context_processors.media",  
  6.                                "django.contrib.messages.context_processors.messages",  
  7.                                "djutils.context_processors.dj_settings",  
  8.                                "djutils.context_processors.current_site",  
  9.                                )  

Теперь в любом темплейте, использующим RequestContext, мы получаем значение вышеуказанных переменных.

Вернемся к шаблону base.html. Код шаблона приводить не буду из-за размеров. Его примерное содержание можно подсмотреть здесь. К нему простенький css. Для того, чтобы css отдавался как статика при разработке на встроенном веб-сервере django, пропишем в urls.py:
  1. from django.conf import settings  
  2. if settings.DEBUG:  
  3.     urlpatterns += patterns('',  
  4.         (r'^media/(?P<path>.*)$''django.views.static.serve', {'document_root': settings.MEDIA_ROOT}),  
  5.     )  
  6. </path>  

а в общие настройки пропишем ADMIN_MEDIA_PREFIX="admin", ибо media по умолчанию занята ADMIN_MEDIA_PREFIX и в случае, если мы ее не переопределим, наша статика работать не будет.

Для сжатия css, а также для перезагрузки закешированного браузером css файла в случае его обновления установим приложение django-compressor:
  1. $ sudo pip install BeautifulSoup  
  2. $ sudo apt-get install git-core  
  3. $ 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.
Добавим в настройки:
  1. COMPRESS = True  
  2. COMPRESS_URL = STATIC_URL  
  3. COMPRESS_ROOT = STATIC_ROOT  
  4. COMPRESS_CSS_FILTERS = [  
  5.      'compressor.filters.cssmin.CSSMinFilter'  
  6. ]  
  7. COMPRESS_JS_FILTERS = [  
  8.      'compressor.filters.jsmin.JSMinFilter'  
  9. ]  

В случае отсутствия переменной COMPRESS в настройках проекта - приложением используется переменная DEBUG, поэтому, если вы хотите отключить сжатие на время разработки, просто закомментируйте COMPRESS.
Используем встроенные в приложения фильтры. Также можно использовать фильтры от yahoo или google.
После того как приложение создаст каталог CACHE, добавим его в svn:ignore.
Теперь попробуем использовать этот шаблон. Для начала отключим MAINTENANCE_MODE.
Исправим файл url.py
  1. from django.conf.urls.defaults import *  
  2. from views import home_page  
  3.   
  4. urlpatterns = patterns('',  
  5.             url(r'^$', home_page, name="home"),  
  6. )  

Создадим файл /src/view.py для проекта:
  1. # -*- mode: python; coding: utf-8; -*-  
  2. from annoying.decorators import render_to  
  3.  
  4. @render_to('homepage.html')  
  5. def home_page(request):  
  6.      
  7.     return {}  

В этом виде используется декоратор функции @render_to. Он поставляется с приложением django-annoying. Установим, добавим в requirements.txt, просмотрим список возможностей (AutoOneToOne field кстати нам пригодится в приложении users).
homepage.html пока содержит следующие вещи:
  1. {% extends "base.html" %}  
  2. {% load i18n %}  
  3. {% block title %}{% trans "Главная страница" %}{% endblock %}  

При создании темплейтов сразу закладываем возможность будущей интернационализации.
Для работы с html кодом используем firebug и HTML VALIDATOR. Также я использовал тег {% spaceless %} в base.html, чтобы сжать выдаваемый html.

Итак, вернемся к users. Приложение users будет хранить дополнительные поля профилей, и использовать сторонние приложения для регистрации и авторизации по open id. Создадим приложение:



Переместим его в apps и создадим модель, например, такую:
  1. # -*- coding:utf-8 -*-  
  2. from django.db import models  
  3. from django.contrib.auth.models import User  
  4.   
  5. from django.utils.translation import ugettext_lazy as _  
  6.   
  7. from annoying.fields import AutoOneToOneField#@UnresolvedImport  
  8.   
  9. GENDER_CHOICES = (  
  10.     ('M''Мужской'),  
  11.     ('F''Женский'),  
  12. )  
  13.   
  14. class UserProfile(models.Model):  
  15.     user = AutoOneToOneField(User, related_name='user_profile', primary_key=True)  
  16.     date_birth = models.DateField(verbose_name=_(u'Дата Рождения'), blank=True, null=True)  
  17.     gender = models.CharField(verbose_name=_(u'Пол'), max_length=1, choices=GENDER_CHOICES, blank=True, null=True)  
  18.     URL = models.URLField(max_length=150, verbose_name=_(u'Ваш сайт'), blank=True, null=True, verify_exists=False)  
  19.     ICQ = models.CharField(max_length=30, verbose_name=u'ICQ', blank=True, null=True)  
  20.     skype = models.CharField(max_length=100, verbose_name=u'skype', blank=True, null=True)  
  21.     jabber = models.CharField(max_length=100, verbose_name=u'jabber', blank=True, null=True)  
  22.     mobile = models.CharField(max_length=100, verbose_name=_(u'Мобильный телефон'), blank=True, null=True)  
  23.     about = models.TextField(verbose_name=_(u'О себе'), help_text=_(u'Несколько слов о себе.'), blank=True, null=True)  
  24.       
  25.     count_login = models.IntegerField(default=0)  
  26.       
  27.     last_activity_ip = models.IPAddressField(null=True)  
  28.     last_activity_date = models.DateTimeField(null=True)  
  29.       
  30.     class Meta:  
  31.         verbose_name = _(u'Профиль пользователя')  
  32.         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 сайта
  1. urlpatterns = patterns('',  
  2.             url(r'^$', home_page, name="home"),  
  3.             (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 бекенда:
  1. from registration.backends.default import DefaultBackend#@UnresolvedImport  
  2.   
  3. from users.forms import DJRegistrationForm#@UnresolvedImport  
  4.   
  5. class DjBackend(DefaultBackend):  
  6.   
  7.     def get_form_class(self, request):  
  8.         """ 
  9.         Return the default form class used for user registration. 
  10.         """  
  11.         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 с одной фукнцией для самостоятельного написания:
  1. @login_required  
  2. @render_to('users/edit_profile.html')  
  3. def edit_profile(request):  

urls.py также самый обычный, самостоятельно.
Так как рендериг формсета и других форм по умолчанию нас не устраивает, создадим два подключаемых темплейта в директории djutils/templates/forms_render:
formset_table.html
  1. {{ formset.management_form }}  
  2. {% for form in formset.forms %}  
  3. {% include "forms_render/form_table.html" %}  
  4. {% endfor %}  

form_table.html
  1. table  
  2. {% for field in form %}  
  3.     {% if not field.is_hidden %}  
  4.     <span class="arial-bold-90">{{ field.label_tag }}</span>{{ field }}{{ field.errors }}  
  5.     <div class="hint">{{ field.help_text }}</div>  
  6.     {% else %}  
  7.     {{ field }}  
  8.     {% endif %}      
  9. {% endfor %}  
  10. /table  

table пришлось оставить без кавычек, иначе blogspot выводит нечто непонятное.
Соответственно,
  1. {% include "forms_render/formset_table.html" %}  

в темплейте users/edit_profile.html .

forms.py для приложения users получился такой:
  1. # -*- coding: utf-8 -*-  
  2. from django.forms import ModelForm  
  3. from django import forms  
  4.   
  5. from django.utils.translation import ugettext_lazy as _  
  6.   
  7. from django.contrib.auth import forms as auth_forms  
  8. from django.contrib.auth.models import User  
  9.   
  10. from users.models import UserProfile#@UnresolvedImport  
  11.   
  12. from registration.forms import RegistrationFormUniqueEmail#@UnresolvedImport  
  13.     
  14. class DJRegistrationForm(RegistrationFormUniqueEmail):  
  15.     def __init__(self, *args, **kwargs):  
  16.         super(DJRegistrationForm, self).__init__(*args, **kwargs)  
  17.         self.fields['username'].widget.attrs["size"] = 65  
  18.         self.fields['email'].widget.attrs["size"] = 65  
  19.         self.fields['password1'].widget.attrs["size"] = 65  
  20.         self.fields['password2'].widget.attrs["size"] = 65  
  21.   
  22.   
  23. class AuthForm(auth_forms.AuthenticationForm):  
  24.     def __init__(self, *args, **kwargs):  
  25.         super(AuthForm, self).__init__(*args, **kwargs)  
  26.         self.fields['username'].widget.attrs["size"] = 65  
  27.         self.fields['password'].widget.attrs["size"] = 65  
  28.   
  29. class PassResetForm(auth_forms.PasswordResetForm):  
  30.     def __init__(self, *args, **kwargs):  
  31.         super(PassResetForm, self).__init__(*args, **kwargs)  
  32.         self.fields['email'].widget.attrs["size"] = 65  
  33.   
  34. class EditProfileForm(ModelForm):  
  35.       
  36.     date_birth = forms.DateField(('%d.%m.%Y',), label=_('Дата рождения'), required=False,    
  37.         widget = forms.DateInput(format='%d.%m.%Y', attrs={  
  38.             'class':'input',  
  39.             'size':'65'  
  40.         })  
  41.     )  
  42.       
  43.     def __init__(self, *args, **kwargs):  
  44.         super(EditProfileForm, self).__init__(*args, **kwargs)  
  45.         self.fields['ICQ'].widget.attrs["size"] = 65  
  46.         self.fields['URL'].widget.attrs["size"] = 65  
  47.         self.fields['jabber'].widget.attrs["size"] = 65  
  48.         self.fields['mobile'].widget.attrs["size"] = 65  
  49.         self.fields['skype'].widget.attrs["size"] = 65  
  50.         self.fields['about'].widget.attrs["cols"] = 49  
  51.         self.fields['about'].widget.attrs["rows"] = 8  
  52.       
  53.     class Meta:  
  54.         model = UserProfile  
  55.         fields = ['gender''date_birth''ICQ''URL''jabber''mobile''skype''about' ]  
  56.           
  57.         #не работает http://code.djangoproject.com/ticket/13095  
  58.         #widgets = {  
  59.         #            'date_birth': forms.DateInput(format="%d.%m.%Y"),   
  60.         #           }  

Для проверки выключаем бекенд вывода писем в консоль, добавляем конфигурацию smtp сервера:
  1. EMAIL_HOST='smtp.server.ru'  
  2. EMAIL_HOST_USER='musicmans.ru'  
  3. EMAIL_HOST_PASSWORD='password'  
  4. DEFAULT_FROM_EMAIL='musicmans.ru@server.ru'  
  5. SERVER_EMAIL='musicmans.ru@server.ru'  

Регистрируемся, выполняем команду django-mailer - manage.py send_mail. Проверяем почту.

Создадим crontab для сервера в develop и можно сразу прописать на сервере (отправка почты (раз в пять минут), повторная отправка (раз в двадцать минут), удаление неактивных пользователей (раз в сутки); будем добавлять вручную, ибо не так часто требуется):
  1. */5 *   * * *   vermus  (/usr/bin/python /srv/musicmans/root/src/manage.py send_mail >> /srv/musicmans/logs/cron_mail.log 2>&1)  
  2. 0,20,40 *       * * *   vermus  (/usr/bin/python /srv/musicmans/root/src/manage.py retry_deferred >> /srv/musicmans/logs/cron_mail_deferred.log 2>&1)  
  3. 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

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

Устанавливаем:
  1. C:\>c:\Python26\Scripts\pip.exe install django-maintenancemode  
  2. Downloading/unpacking django-maintenancemode  
  3.   Downloading django-maintenancemode-0.9.2.tar.gz  
  4.   Running setup.py egg_info for package django-maintenancemode  
  5. Installing collected packages: django-maintenancemode  
  6.   Running setup.py install for django-maintenancemode  
  7. Successfully installed django-maintenancemode  
  8. 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 по своему желанию.

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

Устанавливаем и настраиваем фаерволл:
  1. $ sudo aptitude install ufw  
  2. $ sudo ufw enable  
  3. $ sudo ufw logging on  
  4. $ sudo ufw allow 80/tcp  
  5. $ sudo ufw allow SSH_port  
  6. $ sudo ufw default deny  

Настройку веб сервера выбрал такую (nginx + uwsgi). Тем более, nginx, начиная с версии 0.8.40 поддерживает uwsgi из коробки.
  1. # apt-get install gcc libssl-dev libpcre++-dev make  
  2. # wget http://sysoev.ru/nginx/nginx-0.8.44.tar.gz  
  3. # tar -xzvf nginx-0.8.44.tar.gz  
  4. # cd nginx-0.8.44/  
  5. # ./configure --conf-path=/etc/nginx/nginx.conf \  
  6.   --prefix=/usr \  
  7.   --error-log-path=/var/log/nginx/error.log \  
  8.   --pid-path=/var/run/nginx.pid \  
  9.   --lock-path=/var/lock/nginx.lock \  
  10.   --http-log-path=/var/log/nginx/access.log \  
  11.   --with-http_dav_module \  
  12.   --http-client-body-temp-path=/var/lib/nginx/body \  
  13.   --with-http_ssl_module \  
  14.   --http-proxy-temp-path=/var/lib/nginx/proxy \  
  15.   --with-http_stub_status_module \  
  16.   --http-fastcgi-temp-path=/var/lib/nginx/fastcgi \  
  17.   --http-uwsgi-temp-path=/var/lib/nginx/uwsgi \  
  18.   --http-scgi-temp-path=/var/lib/nginx/scgi \  
  19.   --with-debug \  
  20.   --with-http_flv_module   
  21. # make  
  22. # make install  

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

Создаем рабочую директорию для сайта, например /srv/musicmans
Структура:
  1. /srv/musicmans   
  2. |  backups  
  3. --|  src  
  4. --|  db  
  5. |  logs  
  6. |  root  
  7. --|  src  
  8. --|  www  

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

  1. import os  
  2. import sys  
  3. import locale  
  4. import django.core.handlers.wsgi  
  5.   
  6. DIR=(os.path.abspath(__file__))  
  7. sys.path.append(DIR)  
  8. os.environ['DJANGO_SETTINGS_MODULE'] = 'settings.production'  
  9.   
  10. def force_utf8_hack():  
  11.   reload(sys)  
  12.   sys.setdefaultencoding('utf-8')  
  13.   for attr in dir(locale):  
  14.     if attr[0:3] != 'LC_':  
  15.       continue  
  16.     aref = getattr(locale, attr)  
  17.     locale.setlocale(aref, '')  
  18.     (lang, enc) = locale.getlocale(aref)  
  19.     if lang != None:  
  20.       try:  
  21.         locale.setlocale(aref, (lang, 'UTF-8'))  
  22.       except:  
  23.         os.environ[attr] = lang + '.UTF-8'  
  24.   
  25. force_utf8_hack()  
  26.   
  27.   
  28. application = django.core.handlers.wsgi.WSGIHandler()  

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



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

Добавим в development.py:
  1. INSTALLED_APPS += (  
  2.                    'django.contrib.admin',  
  3.                    )  

А также в manage.py:
  1. if settings.DEBUG and command == "test":  
  2.     settings.MAINTENANCE_MODE = False  
  3.   
  4. execute_manager(settings)  

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

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

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

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

$export SVN_SSH="ssh -l loginname"

или сделаем пару ключа (она нам все рано пригодиться при использовании fabric). На сервере с сайтом:
  1. $ ssh-keygen -t dsa  
  2. $ cat ~/.ssh/id_dsa.pub  

копируем вывод, добавляем на сервер с кодом в ~/.ssh/authorized_keys2 на сервер (если файла нет, то touch ~/.ssh/authorized_keys2 && chmod 600 ~/.ssh/authorized_keys2 ). Пробуем логиниться без пароля.
  1. $svn checkout --depth=empty svn+ssh://codesrv/repos/musicmans/trunk/backend root  
  2. $cd root/  
  3. $svn update --set-depth=infinity www  
  4. $svn update --set-depth=infinity src  

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

Устанавливаем необходимые приложения для сайта:
  1. vermus@musicmans:~$ cd /srv/musicmans/root/src  
  2. vermus@musicmans:~$ sudo pip install -r requirements.txt --download-cache /usr/src/pipcache/  

Установка postgresql:
  1. # apt-get install postgresql python-psycopg2  
  2. # su postgres  
  3. $ createuser musicmans --no-superuser --no-createdb --no-createrole --login --pwprompt --encrypted  
  4. $ createdb --owner=musicmans --encoding=utf-8 musicmans  

База создана, пробуем синхронизировать django с базой данных (мы это делали уже на машине разработчика, но так как у нас база на сайте будет жить своей жизнью, а девелоперская своей, то сделаем это еще раз, т.е. миграцию данных выполнять не будем):
  1. vermus@musicmans:~$ cd /srv/musicmans/root/src/  
  2. vermus@musicmans:/srv/musicmans/root/src$ python manage.py syncdb  

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

Установим uwsgi сервер:
  1. $ cd /usr/src/  
  2. $ sudo pip install http://projects.unbit.it/downloads/uwsgi-latest.tar.gz  

Настроим скрипт init.d для запуска через файловый сокет сервера uwsgi с проектом (/etc/init.d/uwsgi):
  1. # cat uwsgi  
  2. ### BEGIN INIT INFO  
  3. # Provides:          uwsgi  
  4. # Required-Start:    $all  
  5. # Required-Stop:     $all  
  6. # Default-Start:     2 3 4 5  
  7. # Default-Stop:      0 1 6  
  8. # Short-Description: starts the uwsgi app server  
  9. # Description:       starts uwsgi app server using start-stop-daemon  
  10. ### END INIT INFO  
  11.   
  12. PATH=/sbin:/bin:/usr/sbin:/usr/bin  
  13. DAEMON=/usr/bin/uwsgi  
  14.   
  15. OWNER=uwsgirun  
  16.   
  17. NAME=uwsgi  
  18. DESC=uwsgi  
  19.   
  20. test -x $DAEMON || exit 0  
  21.   
  22. # Include uwsgi defaults if available  
  23. if [ -f /etc/uwsgi ] ; then  
  24.         . /etc/uwsgi  
  25. fi  
  26.   
  27. set -e  
  28.   
  29. DAEMON_OPTS="--socket /var/lib/nginx/uwsgi/musicmans.sock --chmod-socket -d /srv/musicmans/logs/uwsgi.log --pythonpath $PYTHONPATH --module $MODULE"  
  30.   
  31. case "$1" in  
  32.   start)  
  33.         echo -n "Starting $DESC: "  
  34.         start-stop-daemon --start --chuid $OWNER:$OWNER --user $OWNER \  
  35.                 --exec $DAEMON -- $DAEMON_OPTS  
  36.         echo "$NAME."  
  37.         ;;  
  38.   stop)  
  39.         echo -n "Stopping $DESC: "  
  40.         start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \  
  41.                 --exec $DAEMON  
  42.         echo "$NAME."  
  43.         ;;  
  44.   reload)  
  45.         killall -1 $DAEMON  
  46.         ;;  
  47.   force-reload)  
  48.         killall -15 $DAEMON  
  49.        ;;  
  50.   restart)  
  51.         echo -n "Restarting $DESC: "  
  52.         start-stop-daemon --signal 3 --user $OWNER --quiet --retry 2 --stop \  
  53.                 --exec $DAEMON  
  54.         sleep 1  
  55.         start-stop-daemon --user $OWNER --start --quiet --chuid $OWNER:$OWNER \  
  56.                --exec $DAEMON -- $DAEMON_OPTS  
  57.         echo "$NAME."  
  58.         ;;  
  59.   status)  
  60.         killall -10 $DAEMON  
  61.         ;;  
  62.       *)  
  63.             N=/etc/init.d/$NAME  
  64.             echo "Usage: $N {start|stop|restart|reload|force-reload|status}" >&2  
  65.             exit 1  
  66.             ;;  
  67.     esac  
  68.     exit 0  

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

Обратите внимание, что мы указываем имя модуля python, а не имя файла.
Устанавливаем chmod 755 для скрипта /etc/init.d/uwsgi , загружаем при старте системы:
  1. root@musicmans:/var/lib/nginx# chown -R uwsgirun uwsgi  
  2. root@musicmans:/etc/init.d# chmod 755 uwsgi  
  3. root@musicmans:/etc/init.d# update-rc.d -f uwsgi defaults  
  4. root@musicmans:/etc/init.d# /etc/init.d/uwsgi start  

Конфиги nginx: nginx.conf, стандартный из пакета debian. Конфиг сайта:
  1. root@musicmans:/etc/nginx/sites-available# cat musicmans  
  2. #serving Django.  
  3. upstream django {  
  4.     ip_hash;  
  5.     server unix:/var/lib/nginx/uwsgi/musicmans.sock;  
  6.   }  
  7.   
  8. server {  
  9.      listen      80;  
  10.      server_name musicmans.ru;  
  11.      charset     utf-8;  
  12.      error_log   /srv/musicmans/logs/nginx_error.log   info;  
  13.      access_log /srv/musicmans/logs/nginx_access.log;  
  14.   
  15.      # Django admin media.  
  16.      #location /media/admin/ {  
  17.      #               alias lib/python2.6/site-packages/django/contrib/admin/media/;  
  18.      #               }  
  19.   
  20.      # Your project's static media.  
  21.      location /media/ {  
  22.                   alias /srv/musicmans/root/www/media/;  
  23.                          }  
  24.   
  25.      # Finally, send all non-media requests to the Django server.  
  26.      location / {  
  27.                  uwsgi_pass  django;  
  28.                  include     uwsgi_params;  
  29.                  }  
  30.   
  31.      location ~ /.svn/ {  
  32.            deny all;  
  33.                }  
  34.   
  35.        }  

Включаем сайт
  1. # 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.
  1. #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).

  1. # -*- mode: python; coding: utf-8; -*-  
  2. import sys  
  3. from fabric.api import env, run, prompt, local, get, cd, sudo, require  
  4. from fabric.state import output  
  5. from fabric.contrib.files import uncomment  
  6. import datetime  
  7.   
  8. now = datetime.datetime.now()  
  9.   
  10. def production():  
  11.     #здесь данные об удаленном сервере с сайтом  
  12.     env.environment = "production"  
  13.       
  14.     env.hosts = ['codesrv']  
  15.     env.user = 'vermus'  
  16.     env.path = '/srv/musicmans/root'  
  17.     env.root_path = '/srv/musicmans'  
  18.       
  19.     env.db_name = 'musicmans'  
  20.     env.db_user = 'musicmans'  
  21.       
  22. def deploy():  
  23.     """ 
  24.     In the current version fabfile no initial database creation and configure the virtual server host. 
  25.     """   
  26.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  27.       
  28.     if env.environment == 'production':  
  29.         if "y" != prompt('Are you sure you want to update the production site (test & check in trunk release code!)? (y/[n])?', default="n"):  
  30.             return  
  31.       
  32.     if "y" == prompt('Set MAINTENANCE_MODE (y/n)?', default="y"):  
  33.         maintenance_mode()   
  34.       
  35.     if "y" == prompt('Create database backup? (y/n)?', default="y"):  
  36.         backup_db()  
  37.           
  38.     if "y" == prompt('Create source code backup? (y/n)?', default="y"):  
  39.         backup_src()  
  40.   
  41.     update_from_svn()  
  42.   
  43.     if "y" == prompt('Install the necessary applications (y/n)?', default="n"):  
  44.         install_requirements();  
  45.   
  46.     migrate_database()  
  47.   
  48.     maintenance_mode(set=False)      
  49.   
  50.     restart_webserver()          
  51.   
  52. def install_requirements():  
  53.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  54.     print(" * install the necessary applications...")  
  55.       
  56.     requirements_file = env.path+'/src/requirements.txt'  
  57.       
  58.     args = ['install',  
  59.                 '-r', requirements_file,  
  60.                 '--download-cache''/usr/src/pipcache/'  
  61.                 ]  
  62.       
  63.     sudo('pip %s' % ' '.join(args))  
  64.   
  65. def maintenance_mode(set=True):  
  66.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  67.     print(" * change production.py and restart nginx...")  
  68.     if set:  
  69.         uncomment(env.path+'/src/settings/production.py''MAINTENANCE_MODE = True')  
  70.     else:  
  71.         comment(env.path+'/src/settings/production.py''MAINTENANCE_MODE = True')  
  72.           
  73.     restart_webserver()   
  74.   
  75. def backup_db():  
  76.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  77.     print(" * create database dump...")  
  78.       
  79.     db_name = env.db_name  
  80.     db_user = env.db_user  
  81.   
  82.     backup_file = "backup_%d_%d_%d_%d_%d.sqlgzip" % (now.day, now.month, now.year, now.hour, now.minute)  
  83.     backup_dir = env.root_path+'/backups/db'  
  84.     with cd(backup_dir):  
  85.         run("echo dbpassword | pg_dump -W -U %s -F c %s > %s" % (db_user, db_name, backup_file))  
  86.   
  87. def backup_src():  
  88.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  89.     print(" * create source code backup...")  
  90.     backup_dir = env.root_path+'/backups/src'  
  91.     backup_file = "backup_%d_%d_%d_%d_%d.tar.gz" % (now.day, now.month, now.year, now.hour, now.minute)  
  92.     src_dir = env.path+'/src'  
  93.       
  94.     run("mkdir -p %s" % backup_dir+'/all')  
  95.     run("cp -f -R %s %s" % (src_dir, backup_dir+'/all'))  
  96.     run("cp -f -R %s %s" % (env.path+'/www/static', backup_dir+'/all'))  
  97.       
  98.     with cd(backup_dir):  
  99.         run ('tar -zcf %s %s' % (backup_file, backup_dir+'/all'))  
  100.         run ('rm -f -R %s' % (backup_dir+'/all'))  
  101.           
  102.          
  103. def update_from_svn():  
  104.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  105.     with cd(env.path):  
  106.         run('svn update'#svn checkout сделаем вручную первый раз  
  107.   
  108. def migrate_database():  
  109.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  110.     with cd(env.path+'/src'):  
  111.         run('python manage.py migrate --no-initial-data')  
  112.         run('python manage.py syncdb')  
  113.           
  114. def restart_webserver():  
  115.     require('environment', provided_by=[production])#дописать по желанию dev и stage  
  116.     print(" * restart nginx")  
  117.     sudo('/etc/init.d/uwsgi restart', pty=True)  
  118.     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 следующую строчку:
  1. # "local" is for Unix domain socket connections only  
  2. local   musicmans   musicmans                         md5  
  3. 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 позже.