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

04 декабря 2012

Меняем заголовок ответа в Pyramid/Pylons

Обычно в заголовке находится что то типа:
Content-Length 6657
Content-Type text/html; charset=UTF-8
итд

Изменить заголовок можно например в настройках Apache или nginx или в самом приложении.
Для Pylons пишем WSGI middleware:
class HeaderMiddleware(object):
    '''WSGI middleware'''

    def __init__(self, application):

        self.app = application

    def __call__(self, environ, start_response):

        request = Request(environ)
        resp = request.get_response(self.app)
        resp.headers['X-Frame-Options'] = 'DENY'

        return resp(environ, start_response)

Для Pyramid можно обойтись без WSGI:
#view.py
from pyramid.events import subscriber
from pyramid.events import NewRequest

@subscriber(NewRequest)
def add_header(event):
    response = event.request.response
    response.headers.add('X-Frame-Options', 'DENY')

27 сентября 2012

Перевод шаблонов Jinja в Pylons

Все точно также как и с mako, "Но есть нюансы"©
Добавьте в setup.py
    package_data={'myproject': ['i18n/*/LC_MESSAGES/*.mo']},
    message_extractors={'myproject': [
            ('**.py', 'python', None),
            ('templates/**.html', 'jinja2', None),
            ('public/**', 'ignore', None)]},
Добавьте в lib/base.py
from pylons.i18n.translation import _, ungettext
И что то типа того в config/environment.py
    # Create the Jinja2 Environment
    config['pylons.app_globals'].jinja2_env = Environment(loader=ChoiceLoader(
            [FileSystemLoader(path) for path in paths['templates']]),
            autoescape=True,
            extensions=['jinja2.ext.do', 'jinja2.ext.i18n'])
    config['pylons.app_globals'].jinja2_env.install_gettext_translations(pylons.i18n)
    # Jinja2's unable to request c's attributes without strict_c
    config['pylons.strict_c'] = True
Теперь можно переводить {{ _('Translate me!') }}

27 марта 2012

Pylons обработка csv с веб формы (метод POST)

Небольшой "хинт" как при помощи Pylons обрабатывать CSV файлы, отправленные из формы.
Создаем шаблон
<form action="" method="POST" enctype="multipart/form-data">
CSV file: 

</form>
<table class="simpletable">
{% for data in c.data %}
<tr>
<td>{{ data[0] }}</td>
<td>{{ data[1] }}</td></tr>
{% endfor %}
</table>

Метод контроллера выглядит как то так
# coding=utf8
import pylons
import csv

from cStringIO import StringIO
from pylons import request, response, session, tmpl_context as c, url
from myproject.lib.base import BaseController, render

class CsvController(BaseController):

    def index(self):
        data = request.POST.get('csvfile', '')
        if data:
            data = data.value
        else:
            return render("/example/csv/index.html")

        f = StringIO(data)

        c.data = []
        for row in csv.reader(f):
            c.data.append((row[0], row[5]))

        return render("/example/csv/index.html")

27 февраля 2012

Pylons система плагинов для своего проекта (yapsy)

При проектировании любого проекта, через определенное время, встает проблема маштабируемости, конечно если вы не пишите сайт-визитку или что то подобное. Проект может состоять из одного большого монолитного ядра с множеством контроллеров, сотнями или даже тысячами таблиц и бессчетным количеством разных шаблонов, библиотек итд итп. Под ядром подразумевается та часть проекта(pylons), которую либо нельзя изменить совсем, либо это потребует очень больших усилий. Для того что бы создать гибкий и легко масштабируемый веб(и не только) проект, необходимо иметь небольшое ядро, которое будет выполнять минимальный функционал соответствующий логически завершенной программе. Остальные функции, пусть даже улучшающие программу, нужно выносить за пределы ядра. Вообще я знаю только два способа решения этой проблемы:
  1. это создание API;
  2. создание подключаемых модулей(плагинов).

В этой статье рассмотрим систему плагинов. Для работы понадобится очень неплохая библиотека yapsy и любой проект на pylons(по аналогии можно реализовать это например в Django или web.py или ... bla bla bla). Добавим в проект папку откуда будут браться плагины:
.
├── config
├── controllers
├── forms
├── lib
├── model
├── plugins
├── public
├── tasks
├── templates
└── tests

Добавим пару плагинов в папку plugins:
.
├── autoloads
│   ├── inline
│   │   └── osmMap
│   │   ├── osm_map.py
│   │   └── osm_map.yapsy-plugin
│   └── menu
│   └── about
│   ├── about.py
│   └── about.yapsy-plugin
├── categories
│   ├── __init__.py
│   ├── inline.py
│   └── menu.py
└── __init__.py

Из директорий видно что мы создали два типа плагинов inline и menu. Inline это плагины встраиваемые на страницу, menu это дополнительные пункты в меню. Папки и категории можно формировать как угодно и в любой вложенности. Сами плагины должны иметь ОБЯЗАТЕЛЬНО файл с таким же именем и расширением yapsy-plugin. В нашем случае osm-map выводит OpenStreetMap карту, а about добавляет в меню новый пункт. Дальше я бегло опишу то что у меня получилось, а посмотреть код можно здесь https://github.com/uralbash/pylons_yapsy_plugin. Думаю не имеет смысла описывать все подробно, т.к. в коде много комментариев.
После старта проекта Вы увидите окно с картой и меню "about". Наши плагины автоматически подгрузились.
yapsy plugin example

Теперь перейдем на вкладку "plugin" и отключим OpenStreetMap
yapsy plugin deactivate

Заходим во вкладку "home"
yapsy plugin deactivated
И "О черт Дэвид Блэйн! Верни ее на место!!!".

То же самое с меню при отключении оно пропадет. Отключенные плагины хранятся в БД в таблице DeactivatedPlugins.

P.S.: Для чего это нужно еще раз? Для того например если руководство решило убрать из программы карту OpenStreetMap немедленно. Вы тут же бросились это все выпиливать... да? А через неделю руководство вдруг решило что нужна карта! Вы начинаете ее запиливать обратно итд итп. Мне например проще нажать одну кнопку, чем переписывать MVC. В этом и есть гибкость. Пользуйтесь.

10 февраля 2012

Скрипты работающие в окружении проекта на Pylons

Иногда необходимо написать скрипт который выполняется из консоли и использует окружение проекта на Pylons. Копипастю простой пример с pylonshq. Так-как там есть привычка периодически удалять информацию.

import optparse

import pylons
from paste.deploy import appconfig

from YOURAPP.config.environment import load_environment


if __name__ == '__main__':
    option_parser = optparse.OptionParser()
    option_parser.add_option('--ini',
        help='INI file to use for pylons settings',
        type='str',
        default='development.ini')
    options, args = option_parser.parse_args()

    # Initialize the Pylons app
    conf = appconfig('config:' + options.ini, relative_to='.')
    load_environment(conf.global_conf, conf.local_conf)

    # Now code can be run, the SQLalchemy Session can be used, etc.
    ....

OpenStreetMap, Геокодирование и автодополнение адреса в строке поиска (как у гугла) с помощью Яндекс API :)

Геокоди́рование — процесс назначения географических идентификаторов (таких как географические координаты, выраженные в виде широты и долготы) объектам карты и записям данных.
Например, геокодированием является назначение координат записям, описывающим адрес (улица/дом) или фотографиям (где было сделано фото) или IP-адресам, или любой другой информации, имеющей географический компонент. Геокодированные объекты могут быть использованы в геоинформационных системах.

Создадим карту и строку поиска объекта по адресу, через API Яндекс Карты. Служба Яндекс.Карт предлагает своим пользователям сервис геокодирования. Он позволяет определять координаты и получать сведения о географическом объекте по его названию или адресу и наоборот, определять адрес объекта на карте по его координатам (обратное геокодирование).
Например, по запросу «Москва, ул. Малая Грузинская, д. 27/13» геокодер возвратит географические координаты этого дома: "37.571309, 55.767190" (долгота, широта). И, наоборот, если в запросе указать географические коордианты дома "37.571309, 55.767190", то геокодер вернет его адрес.
К геокодеру можно обращаться как по HTTP-протоколу, так и с помощью JavaScript API. При обращении к геокодеру по HTTP-протоколу ответ может быть сформирован либо в виде XML-документа формата YMapsML, либо в формате JSON.

При обращении к геокодеру по HTTP-протоколу в параметрах запроса требуется указывать API-ключ. Ключ можно получить, заполнив соответствующую форму.
В данном документе описаны параметры HTTP-запроса к геокодеру, его ответ, а также приведены примеры использования.

Как добавить карту на сайт можно посмотреть здесь

Я приведу пример использования в окружении Pylons и шаблонизатора Jinja. При желании переделать скрипты под другие фреймворки не составит труда. Главное понять принципы.

Добавим поле для поиска
input id="searchAddress" name="address" size="73" type="text" />

Напишем AJAX функцию для выпадающего списка при автодополнении (http://www.devbridge.com/projects/autocomplete/jquery/





Создадим контроллер который будет отдавать данные по AJAX запросу
common.py
# coding=utf8
import logging
import pylons

from pyandexmap import geotagging
from pylons import request, response, session, tmpl_context as c, url
from pylons.controllers.util import abort, redirect

from myapp.lib.base import BaseController, render

log = logging.getLogger(__name__)

class CommonController(BaseController):

    def geocodding_ajax(self):
        """ for autocomplete search in Yandex API
        """
        # Ключ для Яндекс карт из development.ini
        key = pylons.config['yandexKey']
        address = ''
        for addr in geotagging.listGeoObject(request.GET['query'], key):
            if address:
                address = address +", '"+addr+"'"
            else:
                address = "['"+addr+"'"

        resp = '''{
         query:'%s',
         suggestions: %s,
         data:[]
        }''' % (request.GET['query'], address+']')
        return resp


Формат ответа выглядит так
{
 query:'Li',
 suggestions:['Liberia','Libyan Arab Jamahiriya','Liechtenstein','Lithuania'],
 data:['LR','LY','LI','LT']
}

pyandexmap устанавливаем из pypi (про этот модуль я напишу позже)
pip install pyandexmap

Все должно заработать, остались только стили


Результат
Яндекс Карты автодополнение через геокодинг

19 декабря 2011

Как поднять demo версию проекта на paster + virtualenv в Debian

Иногда необходимо поднять демо версию своего проекта для тестов. Склонируем наш git проект на сервер где будет демо
git clone --bare ~/myproject ssh://uralbash@myserver/~/my_project.git
Проект склонируется в домашнюю директорию сервера myserver. Ключ --bare означает что клон предназначен только для push или pull т.е. все коммиты мы будем делать у себя локально а потом пушить на сервак. Далее напишем скрипт который будет из нашего bare репозитария создавать проект для запуска демки
rm -r /home/uralbash/my_project
git clone /home/uralbash/my_project.git /home/uralbash/my_project
После этого создаем virtualenv окружение (/home/uralbash/mypythonenv/) и добавляем скрипт запуска в /etc/init.d/my_project.sh
#! /bin/sh
 
### BEGIN INIT INFO
# Required-Start:    $all
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: starts the paster server
# Description:       starts paster 
### END INIT INFO
 
 
PROJECT=/home/uralbash/my_project
PID_DIR=/var/run/my_project/
PID_FILE=/var/run/my_project/paster.pid
LOG_FILE=/home/uralbash/logs/my_project/paster.log
USER=root
GROUP=root
PROD_FILE=/home/uralbash/demo.ini
RET_VAL=0
 
cd $PROJECT
 
case "$1" in
start)
../mypythonenv/bin/paster  serve \
--daemon \
--pid-file=$PID_FILE \
--log-file=$LOG_FILE \
--user=$USER \
--group=$GROUP \
$PROD_FILE \
start
 
;;
stop)
../mypythonenv/bin/paster  serve \
--daemon \
--pid-file=$PID_FILE \
--log-file=$LOG_FILE \
--user=$USER \
--group=$GROUP \
$PROD_FILE \
stop
 
;;
restart)
../mypythonenv/bin/paster  serve \
--daemon \
--pid-file=$PID_FILE \
--log-file=$LOG_FILE \
--user=$USER \
--group=$GROUP \
$PROD_FILE \
restart
 
 
;;
*)
echo $"Usage: $0 {start|stop|restart}"
exit 1
esac
Для того что бы не хранить пароли в репозитарии файл с настройками вынесен отдельно /home/uralbash/demo.ini

И добавляем в крон перезапуск
# выполнять каждый четный час в 00 мин
0 */2 * * *       /etc/init.d/my_project.sh stop > /dev/null
# выполнять каждый четный час в 01 мин
1 */2 * * *       /etc/init.d/my_project.sh start > /dev/null
# выполнять в 9:00
0 9 * * *       /home/uralbash/demo_update.sh > /dev/null

После этого каждый день в 9:00 будет обновляться код и каждые 2 часа перезапускаться сервер (на случай если он полег по какойто причине). Останется только периодически отправлять коммиты.

18 декабря 2011

FormAlchemy KeyError: "Key not found: "

Ошибка в FormAlchemy типа KeyError: "Key not found: u'Task--super_task_id'" лечится обновлением
pip install formalchemy --upgrade
При этом новая версия подтянет бетта версию webob 1.2 с которым pylons 1 еще не работает из-за:
DeprecationWarning: decode_param_names is deprecated and will not be supported starting with WebOb 1.2
Откатимся
pip install webob==1.1.1
Ошибка должна исчезнуть, вот обсуждение google groups

22 ноября 2011

готовим Pylons + WTForms

WTForm простая, но довольно удобная библиотека для создания форм. И еще WTForm очень похожа на формы в Django - одно из немногово что в джанге сделано хорошо. Посмотрим как это работает с Pylons. Для удобства будем хранить формы отдельно
|+config/
|+controllers/
|~forms/
| |~mycontroller/
| | |-__init__.py
| | `-equipments.py
| |+validators/
| `-__init__.py
Создаем форму для редактирования оборудования equipments.py
from myapp.model.meta import Session as s
from myapp.model.mymodel import EquipmentType
from wtforms import Form, TextField, validators
from wtforms.ext.sqlalchemy.fields import QuerySelectField

# Выбор всех разновидностей оборудования для списка type в форме
def all_equipment_types():
    return s.query(EquipmentType).all()

class EditForm(Form):
    ip = TextField('ip address')
    netmask = TextField('network mask')
    mac = TextField('mac address')
    type = QuerySelectField('type of equipment',
                            query_factory=all_equipment_types)
QuerySelectField это поле из расширения WTForm для SQLAlchemy, которое позволяет создавать списки на основе выборок. Также есть расширения для Django и GAE. Инициализируем нашу форму в контроллере и передадим шаблону
from myapp.forms.mycontroller.equipments import EditForm

class EquipmentsController(BaseController):
    ...
    def edit(self, id, format='html'):
        """GET /myapp/equipments/id/edit: Form to edit an existing item"""
        # url('myapp_edit_equipment', id=ID)
        c.equipment = s.query(Equipment).filter(id==id)
        c.form = EditForm(ip=c.equipment.ip, mac=c.equipment.mac,
                        type=c.equipment.equipmenttype,
                        netmask=c.equipment.netmask)

        return render('/myapp/equipments/edit.html')
После передачи одноименных полям параметров в форму, значения этих параметров будут по умолчанию выведены в форме. Напишем шаблон на Jinja
{% extends "base.html" %}

{% block content %}
<form action="{{ url(controller='myapp/equipments', action='update', id=c.equipment.id) }}" method="POST">
<table class="simpletable">
<caption>Edit equipment of {{ c.equipment.equipmenttype }}
    ({{ c.equipment.ip }})</caption>
<tbody>
{% for field in c.form.data %}
<tr><td>{{ c.form[field].label }}</td>
    <td>{{ c.form[field] }}</td>
</tr>
{% endfor %}
<tr><td colspan="5">
<button><img src="/img/common/save.png" />Save</button>
<input type="hidden" name="_method" value="PUT" />
</td></tr>
</tbody>
</table>
</form>
{% endblock %}
В примере поля выводятся в цикле из списка form.data. Я использую REST контроллеры поэтому обновление происходит в методе update
from myapp.forms.mycontroller.equipments import EditForm

class EquipmentsController(BaseController):
    ...
    def update(self, id):
        """PUT /myapp/equipments/id: Update an existing item"""
        # Forms posted to this method should contain a hidden field:
        #    
        # Or using helpers:
        #    h.form(url('myapp_equipment', id=ID),
        #           method='put')
        # url('myapp_equipment', id=ID)
        equipment = s.query(Equipment).filter(id=id)
        if request.POST['ip']:
            equipment.ip = request.POST['ip']
        if request.POST['netmask']:
            equipment.netmask = request.POST['netmask']
        if request.POST['mac']:
            equipment.mac = request.POST['mac']
        type = EquipmentType.by_id(request.POST['type'])
        equipment.equipmenttype = type
        s.commit()

        came_from = str(request.environ.get('HTTP_REFERER', '')) or url('/')

        redirect(came_from)
Последние две строки возвращают пользователя обратно на страницу редактирования. Выглядит это как-то так:
Пример WTForm и Pylons

13 ноября 2011

Pylons + FormAlchemy REST Controller

Для своих REST контроллеров можно использовать, формы FormAlchemy.
Создаем контроллер:
yourproj% paster restcontroller comment comments
Creating yourproj/yourproj/controllers/comments.py
Creating yourproj/yourproj/tests/functional/test_comments.py
Или если нужно в отдельной директории
yourproj% paster restcontroller admin/tracback admin/trackbacks
Creating yourproj/controllers/admin
Creating yourproj/yourproj/controllers/admin/trackbacks.py
Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py

В файле нашего REST контроллера добавим
from formalchemy.ext.pylons.controller import RESTController

И в конце файла обернем его так
# wrap with formalchemy RESTController
CommentsController = RESTController(CommentsController, 'comment', 'comments')

Теперь если закомментировать какой-нибудь из стандартных методов в контроллере(index, new, update, delete, show, edit) он будет браться из контроллера FormAlchemy со стандартными формами. Довольно удобно в разработке.

27 октября 2011

Админка FormAlchemy для Pylons и Jinja

Для SQLAlchemy существуют интерфейсы управления моделями(что-то типа админки). Как минимум их 2, это Rum и formalchemy.ext.pylons.

Рассмотрим FormAlchemy. Вообще как обычно можно прочитать документацию, но я опишу еще как это все состыковать с шаблонами на Jinja и поддержкой полей Postgres таких как mac, cidr, net и т.д..

Создаем контроллер admin
paster controller admin

Далее редактируем его controllers/admin.py
# coding=utf-8
import logging

from pylons import request, response, session, tmpl_context as c, url

from formalchemy.ext.pylons.controller import ModelsController

from webhelpers.paginate import Page

from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ControllerProtector

from myapp.lib.base import BaseController
from myapp import model
from myapp import forms
from myapp.model import meta

log = logging.getLogger(__name__)

class AdminControllerBase(BaseController):
    model = model # where your SQLAlchemy mappers are
    forms = forms # module containing FormAlchemy fieldsets definitions
    def Session(self): # Session factory
        return meta.Session

    ## customize the query for a model listing
    # def get_page(self):
    #     if self.model_name == 'Foo':
    #         return Page(meta.Session.query(model.Foo).order_by(model.Foo.bar)
    #     return super(AdminControllerBase, self).get_page()

AdminController = ModelsController(AdminControllerBase,
                                     prefix_name='admin',
                                     member_name='model',
                                     collection_name='models',)


Дальше добавляем пути в config/routing.py
# ADMIN
# Map the /admin url to FA's AdminController
# Map static files
map.connect('fa_static', '/admin/_static/{path_info:.*}',
            controller='admin', action='static')
# Index page
map.connect('admin', '/admin', controller='admin', action='models')
map.connect('formatted_admin', '/admin.json', controller='admin',
            action='models', format='json')
# Models
map.resource('model', 'models', path_prefix='/admin/{model_name}',
             controller='admin')

И добавим в шаблоны папку myapp/templates/forms можно взять с github.
Правим следующим образом myapp/forms/__init__.py
from pylons.templating import render_mako
from pylons import config
from myapp import model
from myapp.lib.base import render
from formalchemy import config as fa_config
from formalchemy import templates
from formalchemy import validators
from formalchemy import fields
from formalchemy import forms
from formalchemy import tables
from formalchemy.ext.fsblob import FileFieldRenderer
from formalchemy.ext.fsblob import ImageFieldRenderer

fa_config.encoding = 'utf-8'

class TemplateEngine(templates.TemplateEngine):
    def render(self, name, **kwargs):
        return render_mako('/forms/%s.mako' % name, extra_vars=kwargs)
fa_config.engine = TemplateEngine()

class FieldSet(forms.FieldSet):
    pass

class Grid(tables.Grid):
    pass

## Initialize fieldsets

#Foo = FieldSet(model.Foo)
#Reflected = FieldSet(Reflected)

## Initialize grids

#FooGrid = Grid(model.Foo)
#ReflectedGrid = Grid(Reflected)

Сейчас как и пишут в документации вы действительно по адресу http://localhost:5000/admin попадете в админку! Но только если вы не используете особенностей БД, например sqlalchemy.databases.postgresql. Вторая проблема в том что шаблоны админки не встроены в ваши шаблоны.

Для поддержки Postgres полей в FormAlchemy необходимо добавить в файле lib/base.py
from formalchemy.fields import FieldRenderer
from formalchemy.tests import FieldSet

from sqlalchemy.databases import postgresql

class PostgresFieldRenderer(FieldRenderer):
    """render a field as a postgres field"""
    def render(self, **kwargs):
        return h.text_field(self.name, value=self.value, **kwargs)

# fix postgres field in FormAlchemy
# tnx for http://code.google.com/p/formalchemy/issues/detail?id=167
FieldSet.default_renderers[postgresql.CIDR] = PostgresFieldRenderer
FieldSet.default_renderers[postgresql.MACADDR] = PostgresFieldRenderer
FieldSet.default_renderers[postgresql.INET] = PostgresFieldRenderer
Здесь мы создали 3 правила для FormAlchemy как рендерить поля postgresql.CIDR, postgresql.MACADDR и INET. Этот трюк можно проделать со всем чем угодно :) Более подробно здесь http://code.google.com/p/formalchemy/issues/detail?id=167

И наконец встраиваем админку в ваш шаблон.
Для того что бы это сделать придется переписать все шаблоны на jinja и возможно поменять некоторые файлы(как минимум forms/__init__.py). Я приведу более ленивый и простой метод. У меня в шаблонах есть header.html который я инклудом добавляю в base.html в самом начале. Так же и здесь добавим его в начало админки FormAlchemy. В файле lib/base.py
class BaseController(WSGIController):

    def __call__(self, environ, start_response):
        """Invoke the Controller"""
        # WSGIController.__call__ dispatches to the Controller method
        # the request is routed to. This routing information is
        # available in environ['pylons.routes_dict']

        # Костыль с mako шаблонами в админке FormAlchemy
        # FIXME: для чистоты кода, переписать шаблоны с mako на jinja в папке forms
        c.jinja_menu = render('/common/header.html')
И в шаблоне templates/form/restfieldset.mako
  <body>
${c.jinja_menu}
%if isinstance(models, dict): ...
Сразу после <body> добавляем ${c.jinja_menu}. Тем самым в админку уже отдается отрендеренное меню.

В документации показан интерфейс fa.jquery на самом деле будет обычный. Для jquery его еще надо дополнительно пилить, добавлять в controller/admin
from fa.jquery.pylons import ModelsController as ModelsControllerJQ и дальше как сказанно в исходниках https://github.com/FormAlchemy/fa.jquery/blob/master/fa/jquery/pylons.py обернуть наш контроллер. Что то вроде этого:
AdminController = ModelsController(AdminControllerBase,
                                     prefix_name='admin',
                                     member_name='model',
                                     collection_name='models',)
AdminController = ModelsControllerJQ(AdminController,
                                     prefix_name='admin',
                                     member_name='model',
                                     collection_name='models',)

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

Pylons + Routes фикс слэша в конце URL

Если мапить URL'ы вот так:
map.connect('/logs', controller='logs', action='logs')
То при попытке открыть URL /logs/ вместо /logs появится страница 404

Можно конечно делать так:
map.connect('/logs/', controller='logs', action='logs')
map.connect('/logs', controller='logs', action='logs')
Но это жутко неудобно.

По совету stackoverflow можно обойти эту проблему простым редиректом:
map.redirect('/*(url)/', '/{url}',
             _redirect_code='301 Moved Permanently')
Теперь все запросы оканчивающиеся на слэш будут перенаправляться на адрес без слэша.

Pylons + SQLalchemy расширенная модель (Mixin)

Часто при проектировании структуры БД появляется необходимость повторять одни и те же действия с таблицами. Добавлять одинаковые поля, ссылки, счетчики и т.д. Модели в Pylons(SQLAlchemy), как и в большинстве других фреймворках использующих паттерн MVC, являются классом и соответственно могут быть унаследованы от других классов. Это позволяет нам избежать рутинной работы с повторяющимися действиями.

Все расширения для наших моделей будем добавлять в models/common.py
Создадим базовую модель в которой будет поле id, автоматическая генерация названия таблицы (__tablename__) и метод выбора элемента по id (SELECT * FROM table WHERE id=integer):
class Base(object):
    """Базовая модель. Добавляет во всех наследников поле id и атрибут
    __tablename__ который заполняется автоматически. Имя таблицы берется из
    названия класса и переводится в нижний регистр. Таблица наследник имеет по
    умолчанию название и поле id, сильно облегчая жизнь.
    """

    @declared_attr
    def __tablename__(cls):
        if (has_inherited_table(cls) and
            Tablename not in cls.__bases__):
            return None
        return cls.__name__.lower()

    # Method "byId" for use in code like this:
    #   session.query(Table).byId(5)
    #
    # SQL statement like:
    #   SELECT * FROM Table WHERE id = 5;
    @classmethod
    def byId(cls, id) :
        return Session.query(cls).filter_by(id = id).first()

    id =  Column(Integer, autoincrement=True, primary_key=True)
Метод byId сильно сокращает запись в коде например:
before: Session.query(net).filter_by(id = id).first()
after: net.byId(id)

Теперь создадим нашу модель унаследовав все плюшки с базовой модели:
class Net(Base, DeclarativeBase):
    """Net or subnet."""

    cidr = Column(postgresql.CIDR, index = True)
    description = Column(UnicodeText())

    def __init__(self, cidr=''):
        self.cidr = cidr

    def __repr__(self):
        return "%s" % self.cidr
Наша модель связанна с БД при помощи наследования от DeclarativeBase SQLalchemy, имеет название, поле id и метод byId благодаря наследованию от базовой модели Base из файла common.py.

Для более наглядного примера создадим типовую модель для таблиц которые должны содержать служебную информацию. Модель будет добавлять в другие модели поля:
created_by - кто создал
updated_by - последний кто обновил
created_at - дата создания
updated_at - дата последнего обновления
Поля заполняются автоматически. Кто создал и обновил ссылаются на модель auth.User. Пользователь берется из текущей сессии, при помощи библиотеки lib.auth и метода get_user.Откуда взялась модель User можно узнать из этой статьи Авторизация в Pylons за 5 мин при помощи repoze.what.
class CreatedMixin(object):
    """Абстрактная примесь которая добавляет в другие модели поля:
        created_by - кто создал
        updated_by - последний кто обновил
        created_at - дата создания
        updated_at - дата последнего обновления
    Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
    auth.User. Пользователь берется из текущей сессии, при помощи библиотеки 
    lib.auth и метода get_user
    """

    @declared_attr
    def created_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    @declared_attr
    def updated_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    created_at = Column(DateTime, nullable=False, default=dt.now())
    updated_at = Column(DateTime, nullable=False, default=dt.now(),
                        onupdate=dt.now())
Внешние ссылки и другие атрибуты отличающиеся от обычных полей нужно добавлять при помощи декоратора declare_attr. Теперь меняем нашу модель Net просто добавив CreateMixin:
class Net(Base, DeclarativeBase, CreatedMixin):

Вот полный листинг common.py:
# coding=utf-8
"""Модуль с типовыми моделями
"""

from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relation, relationship
from sqlalchemy.types import Integer, String, DateTime
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import has_inherited_table

from gottlieb.model.auth import User
from gottlieb.lib import auth

from datetime import datetime as dt

class Base(object):
    """Базовая модель. Добавляет во всех наследников поле id и атрибут
    __tablename__ который заполняется автоматически. Имя таблицы берется из
    названия класса и переводится в нижний регистр. Таблица наследник имеет по
    умолчанию название и поле id, сильно облегчая жизнь.
    """

    @declared_attr
    def __tablename__(cls):
        if (has_inherited_table(cls) and
            Tablename not in cls.__bases__):
            return None
        return cls.__name__.lower()

    # Method "byId" for use in code like this:
    #   session.query(Table).byId(5)
    #
    # SQL statement like:
    #   SELECT * FROM Table WHERE id = 5;
    @classmethod
    def byId(cls, id) :
        return Session.query(cls).filter_by(id = id).first()

    id =  Column(Integer, autoincrement=True, primary_key=True)

class CreatedMixin(object):
    """Абстрактная примесь которая добавляет в другие модели поля:
        created_by - кто создал
        updated_by - последний кто обновил
        created_at - дата создания
        updated_at - дата последнего обновления
    Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
    auth.User. Пользователь берется из текущей сессии, при помощи библиотеки 
    lib.auth и метода get_user
    """

    @declared_attr
    def created_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    @declared_attr
    def updated_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    created_at = Column(DateTime, nullable=False, default=dt.now())
    updated_at = Column(DateTime, nullable=False, default=dt.now(),
                        onupdate=dt.now())

Для полной картины приведу аналогичный пример на Django + DjangoORM.
Создадим файл myapp/accompaniment/models.py
from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class ExtendedModel(models.Model):
    created_by = models.ForeignKey(User, null=True, blank=True, 
                 editable=False, related_name='%(class)s_creator')
    created_time = models.DateTimeField(auto_now_add=True, editable=False)
    modified_by = models.ForeignKey(User, null=True, blank=True, 
                 editable=False, related_name='%(class)s_modifier')
    modified_time = models.DateTimeField(auto_now=True, editable=False)

    class Meta:
        abstract = True
В папке accompaniment я привык держать всякие такие хелперы для проекта. Теперь используем эту модель в нашем проекте myapp/projectname/models.py
from django.db import models
from accompaniment.models import ExtendedModel

class Ticket(ExtendedModel):

    OPEN_STATUS = 1
    REOPENED_STATUS = 2
    RESOLVED_STATUS = 3
    CLOSED_STATUS = 4
    DUPLICATE_STATUS = 5

    STATUS_CHOICES = (
        (OPEN_STATUS, _('Open')),
        (REOPENED_STATUS, _('Reopened')),
        (RESOLVED_STATUS, _('Resolved')),
        (CLOSED_STATUS, _('Closed')),
        (DUPLICATE_STATUS, _('Duplicate')),        
    )

    PRIORITY_CHOICES = (
        (1, _('1. Critical')),
        (2, _('2. High')),
        (3, _('3. Normal')),
        (4, _('4. Low')),
        (5, _('5. Very Low')),
    )

    title = models.CharField(
        _('Title'),
        max_length=200,
        )

    queue = models.ForeignKey(
        Queue,
        verbose_name=_('Queue'),
        )

    assigned_to = models.ForeignKey(
        User,
        related_name='assigned_to',
        blank=True,
        null=True,
        verbose_name=_('Assigned to'),
        )

    status = models.IntegerField(
        _('Status'),
        choices=STATUS_CHOICES,
        default=OPEN_STATUS,
        )

    description = models.TextField(
        _('Description'),
        blank=True,
        null=True,
        help_text=_('The content of the customers query.'),
        )

    priority = models.IntegerField(
        _('Priority'),
        choices=PRIORITY_CHOICES,
        default=3,
        blank=3,
        )

    class Meta:
        get_latest_by = "created"
        verbose_name = u'Заявки'
        verbose_name_plural = u'Заявки'
   
    def __unicode__(self):
        return u'%s' % self.title

    def save(self, force_insert=False, force_update=False):
        if not self.priority:
            self.priority = 3

        super(Ticket, self).save(force_insert, force_update)

Такой несложный метод освобождает нас от размножения кучи одинаковых полей в моделях.

Update: в моделях Mixin был атрибут __abstract__ = True это неправильно, так-как все таблицы стают абстрактными. НО! Это вполне прокатит на версиях меньше 0.7, там этот атрибут почему-то не учитывается. На 0.7 версии работает как надо. Вот описание проблемы: stackoverflow

26 октября 2011

Авторизация в Pylons за 5 мин при помощи repoze.what

Статья по сути вольный перевод PylonsTemplates: extra Paster templates for Pylons apps с моими дополнениями.

PylonsTemplates дает вам дополнительные шаблоны в paster для приложений на Pylons. После установки PylonsTemplates можно создать новый проект на Pylons примерно так:
paster create -t [templatename] [projectname]

pylons_repoze_what


Шаблон pylons_repoze_what добавляет систему авторизации основанную на repoze.what и repoze.what-quickstart. (При этом аутентификация на repoze.who устанавливается автоматически.)
Шаблон включает в себя:
* Модели User, Group и Permission для SQLALchemy
* Контроллер login (& logout)
* Простой шаблон для входа
* Зависимость от repoze.what-pylons, включающая декораторы которые можно использовать в контроллерах и действиях(action).
* Закоментированный код в websetup.py который создает user, group и permission.

Список всех шаблонов:
paster create --list-templates

Создание проекта:
paster create -t pylons_repoze_what myapp

Пример с repoze.what-pylons


После создания проекта используя paster create -t pylons_repoze_what, вам нужно защитить контроллеры и их действия от несанкционированного доступа. Ниже простой пример:
from repoze.what.predicates import has_permission
from repoze.what.plugins.pylonshq import ActionProtector

class HelloWorldController(BaseController):
    @ActionProtector(has_permission('be_cool'))
    def index(self):
        return 'Hello World'
Здесь запрещен доступ к действию index всем у кого нет прав 'be_cool'. Что бы запретить весь контроллер есть декоратор ControllerProtector. Более подробно об этом можно почитать в документации на repoze.what-pylons.

У меня в шаблонах отображается постоянно кто сейчас залогирован. Что бы это сделать, создадим lib/auth.py
from pylons import request

def get_user(default):
    """Return the user object from the `repoze.who` Metadata Plugin

    :param default: default item to send back if user not logged in

    Since we might not be logged in and template choke on trying to output
    None/empty data we can pass in a blank User object to get back as a default
    and the templates should work ok with default empty values on that

    """
    if 'repoze.who.identity' in request.environ:
        return request.environ['repoze.who.identity']['user']
    else:
        return default
И добавим контекстную переменную во все контроллеры при помощи lib/base.py
from gottlieb.model.auth import User
from gottlieb.lib import auth

class BaseController(WSGIController):

    def __call__(self, environ, start_response):
        """Invoke the Controller"""
        # WSGIController.__call__ dispatches to the Controller method
        # the request is routed to. This routing information is
        # available in environ['pylons.routes_dict']

        # if there's no user set, just setup a blank instance
        c.current_user = auth.get_user(User())

        try:
            return WSGIController.__call__(self, environ, start_response)
        finally:
            meta.Session.remove()

Теперь можно в шаблоны вставлять что то вроде того(примеры на Jinja):
{% if c.current_user.user_name %} {{ c.current_user.user_name }} (logout) {% else %} login {% endif %}
или
{% for group in c.current_user.groups %}
    {% if group.group_name == 'admin' %}
        

  • Admin
  • {% endif %} {% endfor %}

    отображение залогированного пользователя

    форма авторизации

    Простые хлебные крошки для Pylons и не только...

    В интернете множество советов как делать хлебные крошки. Для Pylons эти советы довольно запутанные, сложные в реализации и рутинны в поддержке на мой взгляд. Поэтому я рассмотрю простой способ парсить URI. Нас будет интересовать только URN (та часть которая идет после названия сайта).

    Пример:
    URI: http://example.ru/js/scripts/test
    URN: js/scripts/test
    breadcrumbs: js >> scripts >> test

    Есть хороший пример http://www.webreference.com/js/scripts/breadcrumbs/, но он отображает в последней крошке(текущей) не распарсенный URN, а название сайта document.title. Это не очень удобно в некоторых случаях.

    Вот мой вариант этого решения:
    добавляем public/js/breadcrumbs.js

    function breadcrumbs(){
      sURL = new String;
      bits = new Object;
      var x = 0;
      var stop = 0;
      var output = "<a href=\"/\">Home</a> &nbsp;&#187;&nbsp; ";
      var end = "";
      sURL = location.href;
      sURL = sURL.slice(8,sURL.length);
      chunkStart = sURL.indexOf("/");
      sURL = sURL.slice(chunkStart+1,sURL.length)
      while(!stop){
        chunkStart = sURL.indexOf("/");
        if (chunkStart != -1){
          bits[x] = sURL.slice(0,chunkStart)
          sURL = sURL.slice(chunkStart+1,sURL.length);
        }else{
          chunkStart = sURL.length;
          end = sURL.slice(0,chunkStart)
          stop = 1;
        }
        x++;
      }
    
      for(var i in bits){
        output += "<a href=\"";
        for(y=1;y<x-i;y++){
          output += "../";
        }
        output += bits[i] + "/\">" + bits[i] + "</a> &nbsp;&#187;&nbsp;";
      }
    
      document.write(output + end);
    }
    

    В шаблон вставляем следующий код:
    
    

    breadcrumbs + javascript

    25 октября 2011

    Pylons javascript и css link

    В Pylons, в шаблоны есть возможноть вставить CSS при помощи WebHelpers - stylesheetlink. Но существует расширение MinificationWebHelpers которое позволяет также удобно добавлять javascript файлы.

    Установка:
    pip install MinificationWebHelpers
    

    Пример использования:
    ${ h.javascript_link('/js/file1.js',
                           '/js/file2.js',
                           minified=True,
                           combined=True,
                           combined_filename='all_javascript_files') }
    ${ h.stylesheet_link('/css/style1.css',
                         '/css/style2.css',
                         minified=True,
                         combined=True,
                         beaker_kwargs=dict(invalidate_on_startup=False)) }
    
    

    Очень удоная штука, особенно когда нужно добавить много файлов.

    24 октября 2011

    SQLalchemy UML диаграмма

    Для визуализации своей базы в SQLAlchemy удобно использовать graphviz и библиотеку sqlalchemy_schemadisplay.
    Установка:
    apt-get install graphviz
    pip install sqlalchemy_schemadisplay
    
    Далее читаем доки SQLAlchemy Schema Display
    Для Ъ:
    Схема БД строится на основании данных базы.

    from sqlalchemy import MetaData
    from sqlalchemy_schemadisplay import create_schema_graph
    
    # create the pydot graph object by autoloading all tables via a bound metadata object
    graph = create_schema_graph(metadata=MetaData('postgres://user:pwd@host/database'),
       show_datatypes=False, # The image would get nasty big if we'd show the datatypes
       show_indexes=False, # ditto for indexes
       rankdir='LR', # From left to right (instead of top to bottom)
       concentrate=False # Don't try to join the relation lines together
    )
    graph.write_png('dbschema.png') # write out the file
    

    Схема БД Postgres

    Схема UML строится по моделям проекта.

    from myapp import model
    from sqlalchemy_schemadisplay import create_uml_graph
    from sqlalchemy.orm import class_mapper
    
    # lets find all the mappers in our model
    mappers = []
    for attr in dir(model):
        if attr[0] == '_': continue
        try:
            cls = getattr(model, attr)
            mappers.append(class_mapper(cls))
        except:
            pass
    
    # pass them to the function and set some formatting options
    graph = create_uml_graph(mappers,
        show_operations=False, # not necessary in this case
        show_multiplicity_one=False # some people like to see the ones, some don't
    )
    graph.write_png('schema.png') # write out the file
    

    Схема моделей в Pylons
     
    Для Django кодеров есть модуль django-extension который добавляет много полезных команд для manage.py. Вот мой вариант скрипта для Django:
    project_dir/_visualozation/visualized.sh
    curent_d="`date +%H%M_%d%m%y`" 
    exec python ../manage.py graph_models -a -g -o scheme_of_$curent_d.png 
    

    пример django-extension + graphviz
    UPD: sadisplay - замечательная штука!