Содержание


Создание чат-приложения с помощью Pyramid, SQLDB и Bluemix

Использование SQLAlchemy для интеграции со службами базы данных в Bluemix

Comments

Это руководство для тех, у кого есть общее представление о языке Python и кто хочет разрабатывать веб-приложения с нуля и развертывать их в IBM® Bluemix®. Вы узнаете, как разработать приложение с использованием среды Pyramid и ее модели безопасности, организовать общение в режиме реального времени с помощью библиотеки абстрагирования Socket.IO и интегрировать все это с помощью шаблона SQLAlchemy. SQLAlchemy предоставляет простой способ интеграции приложений со службами баз данных в среде Bluemix.

Последовательно добавляя одну функцию за другой, мы создадим чат-приложение Chatter, которое позволяет обмениваться сообщениями в режиме реального времени. Сообщения хранятся в базе данных. Открыв приложение, пользователь видит 10 последних сообщений. Вход представляет собой просто процесс идентификации без использования паролей, но предлагаемый подход применим и к системам с типичными требованиями к аутентификации и авторизации.

Что нужно для создания приложения

Запустить приложениеПолучить код

Шаг 1. Установка Pyramid и клиента CF

Для установщиков Ubuntu/Debian:

  1. Загрузите клиент CF в виде Stable Installers для Debian.
  2. Установите клиент CF с правами пользователя root: dpkg -i cf-cli_*.deb.
  3. Установите диспетчер пакетов Python, среду разработки и виртуальную среду с правами пользователя root: apt-get install python-pip python-dev python-virtualenv.
  4. Установите личную виртуальную среду с правами обычного пользователя: virtualenv $HOME/venv.
  5. Активируйте виртуальную среду: . $HOME/venv/bin/activate.

    Указание файла активации гарантирует, что Python и pip будут работать из виртуальной среды.

  6. Установите среду Pyramid: pip install pyramid.

Для установки на Windows®:

  1. Загрузите клиент CF как Stable Installers для Windows.
  2. Разархивируйте и запустите файл cf_installer.exe.
  3. Загрузите установщик Python MSI для своей платформы.
  4. При установке загруженного файла, выберите пункт Install just for me на первой странице и добавьте Add python.exe to Path на странице Customize Python.
  5. Загрузите и установите MS Visual C++ Compiler для Python 2.7.
  6. Откройте командную строку MS-DOS и установите среду Pyramid: pip install pyramid.

Шаг 2. Создание первого проекта Chatter

В Pyramid имеются различные шаблоны (scaffolds). Для создания Pyramid-проекта для этого примера используйте шаблон SQLAlchemy:

pcreate -s alchemy Chatter
cd Chatter

Я заменил текущий каталог только что созданным, потому что в данном руководстве все пути указаны относительно этого каталога.

Используйте следующие файлы:

ФайлОписание
development.iniФайл конфигурации для запуска приложения в режиме разработки
production.iniФайл конфигурации для запуска приложения в производственном режиме
setup.pyСценарий настройки приложения и его упаковки для распространения
chatter/__init__.pyСодержит код настройки приложения
chatter/models.pyСодержит модель приложения (классы управления доступом к базе данных)
chatter/views.pyСодержит методы управления логикой приложения view-callable
Каталог chatter/staticСодержит статические ресурсы, используемые приложением
Каталог chatter/templatesСодержит шаблоны страниц, используемые приложением

В проекте используются ссылки пакетов на себя. Чтобы эти ссылки работали правильно, установите его в режиме разработки; при этом также будут установлены дополнительные зависимости, указанные в списке необходимых зависимостей в файле setup.py:

python setup.py develop

Запустите приложение:

pserve production.ini

Эта команда сообщает, какой процесс выполняет приложение, и URL-адрес, по которому оно доступно, после чего ожидает остановки нажатием комбинации клавиш <Ctrl>-C.

Конфигурация сервера по умолчанию в файле production.ini настроена на прослушивание порта 6543 (см. параметр port в файле), так что к нему можно обращаться из браузера по адресу: http://localhost:6543.

Вместо красивой страницы браузер отображает сообщение о том, что таблицы в базе данных не инициализированы. Я рекомендую не инициализировать таблицы БД отдельным шагом, а сделать это при запуске приложения. В файле chatter/__init__.py замените строку Base.metadata.bind = engine следующей строкой:

    Base.metadata.create_all(bind = engine)

При этом создаются все таблицы, расширяющие класс Base, но только если их еще не было. По умолчанию в файле production.ini для базы данных указано sqlalchemy.url = sqlite:///%(here)s/Chatter.sqlite, что означает, что все данные хранятся в файле Chatter.sqlite. При каждом изменении модели можно просто удалить этот файл перед запуском приложения.

Ввиду отсутствия отдельного шага по созданию таблиц базы данных удалите каталог chatter/scripts:
rm -rf chatter/scripts (Unix) или
rmdir /s/q chatter\scripts (Windows).

На протяжении данного руководства мы будем редактировать статические файлы, поэтому отключите их кэширование (мы включим его в конце). В файле chatter/__init__.py в вызове config.add_static_view() измените параметр cache_max_age=3600 на cache_max_age=0.

Запустите приложение и обратитесь к нему из браузера – вы должны увидеть шаблон страницы приветствия Pyramid SQLAlchemy.

Добавление к модели класса Message

Наше приложение хранит сообщения в базе данных. Используемая структура данных объявляется в файле chatter/models.py. Этот файл уже содержит пример класса, но нам нужно изменить его на что-то полезное для наших целей. Удалите все строки, начиная с class MyModel(Base): и до конца файла. Добавьте вместо них следующие строки:

import datetime

from sqlalchemy import (
    Sequence,
    DateTime,
    )

class Message(Base):
    __tablename__ = 'messages'
    id = Column(Integer, Sequence('msid', optional=True), primary_key=True)
    text = Column(Text, nullable=False)
    created = Column(DateTime, default=datetime.datetime.utcnow)

    def __init__(self, text):
        self.text = text

Этот код объявляет класс для обращения к таблице 'messages' (указанной в атрибуте__tablename__), которая содержит три столбца: id (первичный ключ, тип Integer), text (тип Text) и created (тип DateTime). Этот класс расширяет класс Base (объявленный в файле chatter/model/meta.py), объединяя его со средой SQLAlchemy и добавляя в метаданные, содержащиеся в экземпляре Base. Вызов Sequence() помещает в столбец id автоматически наращиваемое значение.

Атрибут created использует значение по умолчанию времени создания экземпляра. Я использую datetime.datetime.utcnow для сохранения значения времени, не зависящего от часового пояса. Кроме того, обратите внимание, что это ссылка на функцию, а не вызов этой функции. Если превратить ее в вызов, заменив на datetime.datetime.utcnow(), то значением по умолчанию будет время инициализации класса Message, а не время создания каждого экземпляра Message.

Удалив MyModel, удалите и Chatter.sqlite.

Добавление метода view callable сообщения

Метод View callable в Pyramid преобразует запросы артефактов в ответы. Замените содержимое файла chatter/views.py следующим кодом:

from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
import transaction

from .models import (
    DBSession,
    Message,
    )

@view_config(route_name='home', renderer='templates/messages.pt')
def index(request):
    messages = DBSession.query(Message).order_by(Message.created)[-10:]
    return {'messages': messages}

@view_config(route_name='send')
def send(request):
    text = request.params.get('message')
    with transaction.manager:
        message = Message(text)
        DBSession.add(message)
    return HTTPFound(location=request.route_url('home'))

Вызываемый объект index связан с маршрутом 'home', который по умолчанию связан с URL-адресом приложения /. Он извлекает до 10 последних сообщений из базы данных с помощью класса Message, объявленного в файле chatter/model.py, и возвращает их в качестве параметра 'message' для визуализации шаблона templates/messages.pt.

Вызываемый объект send связан с маршрутом 'send' (будет добавлен на следующем шаге). Он обрабатывает отправку запросов из формы в файле templates/messages.pt, просто вставляя новую строку с помощью классов DBSession и Message и возвращая запрос по маршруту 'home'. Перенаправление после передачи методом POST – хороший способ избежать повторной отправки формы, когда пользователь перезагружает страницу. Я использую route_url для определения места переадресации, чтобы код не зависел от URL-маршрута, на который отображается 'home'.

Добавьте в файл chatter/__init__.py отображение маршрута 'send' (добавьте его до строки config.scan()):

    config.add_route('send', '/send')

Добавление шаблона messages.pt

Чтобы исключить ненужные файлы, удалите все шаблоны и статические файлы, прежде чем двигаться дальше:
rm -f chatter/templates/* chatter/static/* (Unix) или
del /q chatter\templates chatter\static (Windows).

По умолчанию для шаблонов используется механизм рендеринга Chameleon. Создайте файл chatter/templates/messages.pt (расширение .pt указывает на то, что он должен обрабатываться механизмом Chameleon) следующего содержания:

<html>
  <head>
    <title>Messages</title>
    <link rel="stylesheet" type="text/css" href="/static/messages.css"/>
  </head>
  <body>
    <div id="chatlog">
      <ul>
        <li tal:repeat="m messages" tal:content="m.text" />
      </ul>
    </div>
    <hr/>
    <form action="${request.route_url('send')}" method="post">
      <input id="send" name="send" type="submit" value="Send" />
      <input id="message" name="message" type="text" />
    </form>
  </body>
</html>

Этот шаблон создает страницу со списком всех сообщений, передаваемых ему из view callable, и формирует сообщения для отправки по маршруту 'send'. Атрибут tal:repeat="m messages" – это способ, при помощи которого Chameleon создает элемент <li> для каждого элемента массива 'messages', используя переменную 'm'. Атрибут tal:content="m.text" указывает механизму применения шаблона, что в элемент (между <li> и </li>) надо вставить значение "m.text".

Чтобы сделать текст больше похожим на чат, чем на неупорядоченный список, создайте таблицу стилей chatter/static/messages.css следующего содержания:

#chatlog > ul {
  padding: 0 0;
  list-style-type: none;
}

Первый вариант приложения готов. Запустите его с помощью команды pserve production.ini и откройте страницу http://localhost:6543. При нажатии кнопки Send отображаются последние 10 сообщений из базы данных. Если к странице обращаются несколько пользователей, они увидят сообщения друг друга.

Чтобы увидеть код до этого места, используйте тег step2-first-draft в Git-репозитории.

Шаг 3. Делаем Chatter более динамичным с помощью SocketIO

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

Создание сценария запуска

Сначала откорректируем способ запуска приложения. Команда pserve не позволяет выбрать сервер, поэтому нужно иметь свой собственный сценарий запуска. Создайте файл serve.py в текущем каталоге со следующими строками:

import os
from paste.deploy import loadapp
from socketio.server import serve
from gevent import monkey; monkey.patch_all()

if __name__ == '__main__':

    app = loadapp('config:production.ini', relative_to=os.getcwd())
    try:
        serve(app, host='0.0.0.0')
    except KeyboardInterrupt:
        pass

Вам также придется установить пакет с библиотекой socketio. Добавьте строку gevent-socketio, в список requires[] в файле setup.py и выполните python setup.py develop еще раз.

Этот сценарий использует для прослушивания запросов реализацию SocketIOServer, вместо стандартной WSGIServer. Теперь проверьте сценарий, запустив python serve.py, чтобы убедиться, что приложение ведет себя по-прежнему.

Редактирование серверной части SocketIO

Чтобы изменить способ обработки входящих запросов методом send, замените декларацию метода send (начиная со строки @view_config(route_name='send') и до конца файла) в файле chatter/views.py следующим кодом:

from socketio import socketio_manage
from socketio.mixins import BroadcastMixin
from socketio.namespace import BaseNamespace
from pyramid.response import Response

@view_config(route_name='send')
def send(request):
    socketio_manage(request.environ, {'/chat': ChatNamespace}, request)
    return Response('')

class ChatNamespace(BroadcastMixin, BaseNamespace):
    def on_chat(self, text):
        with transaction.manager:
            message = Message(text)
            DBSession.add(message)
            self.broadcast_event('chat', text)

Теперь запросы по маршруту send обрабатываются SocketIO особым образом, так что обработка запросов в пространстве имен /chat делегируется классу ChatNamespace. ChatNamespace, в свою очередь, обрабатывает события chat, добавляя сообщения в базу данных и рассылая текст сообщений всем клиентам, подключенным к серверу.

Запросы SocketIO отправляются по специальному пути, начинающемуся с /socket.io/, так что настройте в файле chatter/__init__.py отображение и для маршрута send. Измените строку config.add_route('send', '/send') следующим образом:

    config.add_route('send', '/socket.io/*remaining')

Редактирование клиентской части SocketIO

Перечисленные выше изменения касаются серверной части SocketIO. В клиентской части нужно настроить шаблон и добавить код JavaScript для обработки обмена сообщениями SocketIO.

Измените форму декларации (между элементами <form> и </form>) в файле chatter/templates/messages.pl следующим образом:

    <form id="chatform">
      <input id="send" name="send" type="submit" value="Send" />
      <input id="message" name="message" type="text" />
    </form>
    <script src="//cdn.jsdelivr.net/jquery/2.1.1/jquery.min.js"></script>
    <script src="//cdnjs.cloudflare.com/ajax/libs/socket.io/0.9.16/socket.io.min.js"></script>
    <script src="/static/chat.js" type="text/javascript"></script>

Я добавил id в отправляемую форму, чтобы код JavaScript мог найти ее, и еще добавил набор файлов JavaScript. Файл jquery.min.js упрощает поиск элементов на странице.

Примечание. Используйте socket.io.js версии 0.9.16, так как реализация gevent-socketio поддерживает эту версию или совместима с ней. В версию socket.io.js 1.0 внесены изменения, несовместимые с предыдущими версиями SocketIO.

Обработку SocketIO в приложении осуществляет сценарий chat.js – создайте его в файле chatter/static/chat.js:

$(document).ready(function() {
  var socket = io.connect('/chat')

  $(window).bind("beforeunload", function() {
    socket.disconnect();
  });

  var chatlog = $("#chatlog>ul")

  var addMessage = function(data) {
    chatlog.append($('<li>').append(data));
  }

  socket.on("chat", function(e) {
    addMessage(e);
    chatlog.scrollTop(chatlog.height());
  });

  $("#chatform").submit(function(e) {
    // don't allow the form to submit
    e.preventDefault();

    // send out the "chat" event with the textbox as the only argument
    socket.emit("chat", $("#message").val());
    $("#message").val("");
  });
});

Этот сценарий:

  • подключается к пространству имен '/chat' (то же пространство, которое ChatNamespace отображает на вызываемое представление send);
  • прослушивает события chat (распространяемые посредством ChatNameserver в методе on_chat), добавляя в chatlog элемент '<li>' с полученными данными;
  • создает события chat (управляемые ChatNameserver с помощью метода on_chat) при отправке chatform.

Если теперь запустить приложение командой python serve.py, то будут отображаться сообщения сразу всех пользователей, подключенных к приложению. Обратите внимание, что chatform отправляется с каждым новым сообщением, чего чат-приложения обычно не делают. Добавьте необходимые свойства стиля в файл chatter/static/messages.css (содержимое нового файла):

#chatlog > ul {
  padding: 0 0;
  list-style-type: none;
  height: 200pt;
  overflow-y: scroll;
}
#message {
  width: 80%;
}

Теперь приложение должно быть больше похоже на чат.

Чтобы увидеть код до этого места, используйте тег step3-add-socketio в Git-репозитории.

Шаг 4. Добавление к приложению Chatter новых пользователей

Требование регистрации пользователей

Чтобы присоединиться к чату могли только зарегистрированные пользователи, нужно:

  1. Включить в свое приложение политики проверки подлинности и авторизации.
  2. Потребовать разрешение на доступ к вызываемым представлениям.
  3. Направить пользователей за разрешением.
  4. Добавить представление входа, чтобы пользователи могли самостоятельно авторизоваться.

Добавьте следующие строки в файл chatter/__init__.py перед объявлением функции main:

from pyramid.security import Allow, Everyone
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

class Root:
    __acl__ = [ (Allow, 'group:view', 'view') ]
    def __init__(self, request):
        pass

def groupfinder(userid, request):
    return ['group:view']

Этот код добавляет класс Root, который используется в качестве контекста для всех запросов к приложению. Он дает членам группы 'group:view' разрешение просматривать представление 'view' и запрещает доступ ко всему остальному. Добавлена функция groupfinder для получения списка групп, к которым принадлежит userid. Все пользователи приложения принадлежат к группе 'group:view'.

Чтобы начать использовать эти объекты в приложении, в том же файле (chatter/__init__.py) в функции main замените строку config = Configurator(settings=settings) следующим кодом:

    config = Configurator(root_factory=Root, settings=settings)
    config.set_authentication_policy(AuthTktAuthenticationPolicy(
        'chattersecret', groupfinder, hashalg='sha512'))
    config.set_authorization_policy(ACLAuthorizationPolicy())
    config.set_default_permission('view')
    config.add_route('logout', '/logout')

Теперь класс Root настроен как корневой контекст приложения. Этот код также задает политику использования cookie-файлов auth_tkt для информации аутентификации (через экземпляр AuthTktAuthenticationPolicy), которая шифруется с помощью ключевого слова 'chattersecret' (оно должно быть уникальным для каждого приложения) и хэшируется с помощью алгоритма 'sha512'. Он использует метод groupfinder в качестве обратного вызова, чтобы по информации аутентификации находить группы, к которым принадлежат пользователи. Для политики авторизации код использует ACL, что означает, что способ доступа к объекту контекста указан как атрибут класса '__acl__'.

Если теперь попытаться запустить приложение, то вы получите сообщение об ошибке 403 Forbidden, потому что не предоставили пользователям способа авторизоваться. В файл chatter/views.py нужно добавить представление forbidden_view. Добавьте в конец файла следующие строки.

from pyramid.view import forbidden_view_config
from pyramid.security import remember, forget

@forbidden_view_config(renderer='templates/login.pt')
def login(request):
    error = ''
    if 'login' in request.params:
        username = request.params['username']
        if not username:
            error = "user name can not be empty"
        else:
            headers = remember(request, username)
            return HTTPFound(location=request.route_url('home'),
                headers=headers)
    return {'error': error}

@view_config(route_name="logout")
def logout(request):
    headers = forget(request)
    return HTTPFound(location=request.route_url('home'), headers=headers)

Декоратор forbidden_view_config заставляет приложение вызывать это представление, вместо страницы сообщения об ошибке по умолчанию forbidden. Это же представление используется при отправке страницы регистрации. Для разграничения этих двух событий используется наличие 'login' (имя кнопки отправки формы в файле login.pt). Теперь давайте убедимся, что введенное имя пользователя – не пустая строка. Если это условие выполнено, то данные пользователя сохраняются и обратный вызов перенаправляется к 'home'. В противном случае возвращается страница входа в систему с сообщением об ошибке.

Вызываемое представление logout позволяет пользователю запросить удаление себя из приложения.

Добавьте в файл chatter/templates/login.pt шаблон страницы входа:

<html>
  <head>
    <title>Login</title>
    <link rel="stylesheet" type="text/css" href="/static/login.css" />
  </head>
  <body>
    <span id="error" tal:condition="error" tal:content="error" />
    <form method="post">
Username: <input id="username" name="username" type="text" />
      <input id="login" name="login" value="Login" type="submit" />
    </form>
  </body>
</html>

tal:condition="error" tal:content="error" означает, что этот элемент span добавляется только в том случае, если значение error не пустое. Содержимым элемента является значение error. В остальном это шаблон простой формы входа в систему.

Чтобы выделить сообщения об ошибках, создайте файл chatter/static/login.css:

#error {
color: red;
}

Пользователи должны иметь возможность выйти из приложения, поэтому добавьте в файл chatter/templates/messages.pl после chatform следующий код:

    <form action="${request.route_url('logout')}" method="post">
 ${request.authenticated_userid}: <input id="logout" type="submit" value="Logout" />
    </form>

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

Чтобы увидеть код до этого места, используйте тег step4a-add-login в Git-репозитории.

Добавление в модель класса user

Все эти изменения пока еще не связали сообщения с их авторами. В модель нужно добавить пользователей и связать с ними сообщения. Ниже показаны добавленный класс User и обновленный класс Message в файле chatter/models.py (замените строки, начиная с class Message(Base) и до конца файла):

from sqlalchemy import (
    Unicode,
    ForeignKey,
    )
from sqlalchemy.orm import relation

class Message(Base):
    __tablename__ = 'messages'
    id = Column(Integer, Sequence('msid', optional=True), primary_key=True)
    text = Column(Text, nullable=False)
    created = Column(DateTime, default=datetime.datetime.utcnow)
    userid = Column(Integer, ForeignKey('users.id'), nullable=False)

    def __init__(self, user, text):
        self.user = user
        self.text = text

class User(Base):
    __tablename__ = 'users'
    id = Column(Integer, Sequence('usid', optional=True), primary_key=True)
    name = Column(Unicode(255))
    messages = relation(Message, backref="user")

    def __init__(self, name):
        self.name = name

Обратите внимание на добавление атрибута userid как ForeignKey в столбце 'users.id', указанного через имя таблицы, используемое классом User. То же соотношение используется при объявлении атрибута messages класса User. Вызов relation(Message) создает атрибут запроса, возвращающий массив экземпляров Message, внешний ключ которого соответствует первичному ключу User. Параметр backref="user" приводит к добавлению атрибута User к классу Message, возвращая экземпляр User, на который ссылается внешний ключ.

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

Для настройки представлений login и messages таким образом, чтобы они принимали во внимание информацию пользователя, и во избежание дублирования пользователей храните в запросе userid, вместо username. В файле chatter/views.py замените строку headers = remember(request, username) в декларации метода login следующим кодом:

            with transaction.manager:
                users = DBSession.query(User).filter_by(name=username).all()
                if users:
                    user = users[0]
                else:
                    user = User(name=username)
                    DBSession.add(user)
                    DBSession.flush()
                headers = remember(request, user.id)

Вызов remember должен быть в области with transaction.manager, иначе он не сможет обращаться к атрибутам экземпляра User. Кроме того, добавлен вызов DBSession.flush(), чтобы запрос на добавление пользователя передавался в базу данных для создания нового идентификатора пользователя, что позволит извлекать его из экземпляра user.

В код обработки событий chat нужно внести следующее изменение, потому что экземпляр Message теперь требует указания пользователя. Измененный метод on_chat класса ChatNamespace должен выглядеть следующим образом:

    def on_chat(self, text):
        with transaction.manager:
            user = DBSession.query(User).get(self.request.authenticated_userid)
            if user:
                message = Message(user, text)
                DBSession.add(message)
                self.broadcast_event('chat', text)

Исправьте имя, которое раньше использовалось перед кнопкой Logout. Для этого отправьте имя пользователя в шаблон как еще один входной элемент. Измените декларацию метода index следующим образом:

from .models import User

@view_config(route_name='home', renderer='templates/messages.pt')
def index(request):
    user = DBSession.query(User).get(request.authenticated_userid)
    if user:
        messages = DBSession.query(Message).order_by(Message.created)[-10:]
        return {'messages': messages, 'username': user.name}
    else:
        return logout(request)

Также необходимо изменить файл chatter/templates/messages.pt, добавив следующую строку отправки события нажатия кнопки logout:

${username}: <input id="logout" type="submit" value="Logout" />

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

Чтобы увидеть код до этого места, используйте тег step4b-user-info в Git-репозитории.

Добавление в событие чата данных о пользователе и времени

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

Теперь мы передаем клиенту не простую строку, а более сложную структуру, поэтому нужно отформатировать первоначальную историю сообщений и на стороне клиента. Для этого и во избежание дублирования кода изменим запрос chat таким образом, чтобы он отправлял каждый раз не один объект, а массив объектов сообщений. В файле chatter/views.py замените декларацию класса ChatNamespace следующим кодом:

def mToD(message):
    return {'who': message.user.name,
            'when': message.created.isoformat(),
            'what': message.text}

class ChatNamespace(BroadcastMixin, BaseNamespace):
    def recv_disconnect(self):
        self.disconnect(silent=True)

    def on_history(self):
        self.emit('chat', [mToD(m) for m in DBSession.query(Message).order_by(
                  Message.created)[-10:]])

    def on_chat(self, text):
        with transaction.manager:
            user = DBSession.query(User).get(self.request.authenticated_userid)
            if user:
                message = Message(user, text)
                DBSession.add(message)
                DBSession.flush()
                self.broadcast_event('chat', [mToD(message)])

Также необходимо удалить добавление сообщений в методе index. Измените его следующим образом:

def index(request):
    user = DBSession.query(User).get(request.authenticated_userid)
    if user:
        return {'username': user.name}
    else:
        return logout(request)

Исключим добавление сообщений также и в файле chatter/templates/messages.pt. Удалите следующую строку:

        <li tal:repeat="m messages" tal:content="m.text" />

Вот соответствующие изменения в файле chatter/static/chat.js (только измененные строки с объявлением функции addMessage и обработчиком событий chat):

  var prevDateString = '';

  var addMessage = function(data) {
    currDate = new Date(data.when);
    currDateString = currDate.toDateString()
    if (currDateString !== prevDateString) {
      chatlog.append($('<li>').attr('class', 'date').text(currDateString));
      prevDateString = currDateString;
    }
    chatlog.append($('<li>').append(
      $('<span>').attr('class', 'when').text("["+currDate.toLocaleTimeString()+
                                             "]")).append(" ").append(
      $('<span>').attr('class', 'who').text(data.who + ":")).append(" ").append(
      $('<span>').attr('class', 'what').text(data.what)));
  }

  socket.on("chat", function(e) {
    for (i=0; i<e.length; i++) {
      addMessage(e[i]);
    }
    chatlog.scrollTop(chatlog.height());
  });

  socket.emit("history")

Чтобы лучше видеть разницу между частями сообщения, добавьте в chatter/static/messages.css следующие строки:

.date {
  text-align: center;
  font-size: 140%;
}
.who {
  font-weight: bold;
}
.when {
  font-style: italic;
  color: gray;
}

Теперь попробуйте запустить приложение командой python serve.py.

Чтобы увидеть код до этого места, используйте тег step4c-display-usertime в Git-репозитории.

Добавление информации о присутствии пользователей

В большинстве чат-приложений пользователи могут видеть участников разговора. Для этого добавьте новый класс Connect для хранения сведений о соединениях. Добавьте в файл chatter/models.py следующую декларацию класса:

from sqlalchemy import Boolean

class Connect(Base):
    __tablename__ = 'connects'
    id = Column(Integer, Sequence('cnid', optional=True), primary_key=True)
    userid = Column(Integer, ForeignKey('users.id'), nullable=False)
    time = Column(DateTime, default=datetime.datetime.utcnow)
    ison = Column(Boolean)

    def __init__(self, user, ison):
        self.userid = user.id
        self.ison = ison

Эта таблица содержит запись для каждого события установления и прекращения соединения с пользователем. Поскольку декларации существующих таблиц в модели не изменились, нет необходимости удалять Chatter.sqlite. Новая таблица добавится к существующим таблицам при следующем запуске приложения.

Сведения о состоянии пользователя передаются клиенту в событии status. Данные события содержат массив пользователей и их текущее состояние: 'on' или 'off'. Добавьте обработку события connect с помощью метода recv_disconnect и отредактируйте методы recv_disconnect и on_history в chatter/views.py следующим образом (метод on_chat должен остаться тем же):

from sqlalchemy import func
from .models import Connect

lastIdQuery = DBSession.query(func.max(Connect.id).label('max')).group_by(
              Connect.userid).subquery()
connQuery = DBSession.query(Connect).join(
            lastIdQuery, Connect.id == lastIdQuery.c.max).subquery()
connectedUsers = DBSession.query(User).join(connQuery).filter(connQuery.c.ison)

def disconn_all():
    with transaction.manager:
        for user in connectedUsers:
            DBSession.add(Connect(user, False))

def uToD(user, ison):
    return {'id': user.id,
            'name': user.name,
            'ison': ison}

class ChatNamespace(BroadcastMixin, BaseNamespace):
    def recv_connect(self):
        self._connect_event(True)

    def recv_disconnect(self):
        self._connect_event(False)
        self.disconnect(silent=True)

    def _connect_event(self, ison):
        with transaction.manager:
            user = DBSession.query(User).get(self.request.authenticated_userid)
            if user:
                DBSession.add(Connect(user, ison))
                self.broadcast_event('status', [uToD(user, ison)])

    def on_history(self):
        self.emit('chat', [mToD(m) for m in DBSession.query(Message).order_by(
                  Message.created)[-10:]])
        self.emit('status', [uToD(u, True) for u in connectedUsers])

Уточнение:

  • После событий connect и disconnect в таблицу Connect добавляется новая запись и событие status передается всем пользователям.
  • По запросу history от клиента отправляется список пользователей, подключенных в настоящее время, с событиями status и chat.
  • Что касается connectedUsers, то lastIdQuery возвращает максимальный идентификатор записи Connect каждого пользователя. Метод connQuery возвращает записи из таблицы Connect, соответствующие этим идентификаторам (последняя запись для каждого пользователя). Метод connectedUsers возвращает имена пользователей, у которых значение ison в их последней записи в таблице Connect равно True.

Чтобы запретить повторное подключение одного и того же пользователя, отредактируйте запрос user и проверку в методе login в том же файле chatter/views.py следующим образом (условие else остается тем же):

                users = DBSession.query(User, connQuery.c.ison).join(
                        connQuery).filter(User.name==username).all()
                if users:
                    user = users[0][0]
                    if users[0][1]:
                        return {'error': 'User "%s" is already connected' % user.name}
                else:

При перезапуске сервера установите для всех пользователей отключенное состояние, прежде чем их клиенты подключатся снова. Для этого при запуске приложения необходимо вызывать представленный выше метод disconn_all(). Добавьте следующие строки к файлу chatter/__init__.py после вызова метода create_all(), служащего для создания всех таблиц:

    from .views import disconn_all
    disconn_all()

В клиентской части журнал сообщений отображается рядом со списком пользователей. Замените <div id="chatlog"> в файле chatter/templates/messsages.pt следующей таблицей:

    <table>
      <tr>
        <td id="chatlog">
          <ul/>
        </td>
        <td id="users">
          <ul/>
        </td>
      </tr>
    </table>

Чтобы сделать это представление наглядным, добавьте в chatter/static/messages.css следующие записи:

table {
  width: 100%;
}
#chatlog {
  width: 80%;
  vertical-align: top;
}
#users {
  vertical-align: top;
}
#users > ul {
  padding: 0 0;
  list-style-type: none;
}

Добавьте в файл chatter/static/chat.js обработку событий status, чтобы пользователи добавлялись и удалялись в зависимости от своего статуса ison:

  var users = $("#users>ul");

  var updateStatus = function(data) {
    user = $("#user-" + data.id);
    if ((user.length === 0) === data.ison) {
      if (data.ison) {
        users.append($('<li>').text(data.name).attr('id', "user-" + data.id));
      } else {
        user.remove();
      }
    }
  }

  socket.on("status", function(e) {
    for (i=0; i<e.length; i++) {
      updateStatus(e[i]);
    }
  });

Теперь приложение показывает подключенных пользователей. Мы закончили внесение изменений в статические файлы, так что теперь можно изменить значение cache_max_age=0 в файле chatter/__init__.py обратно на cache_max_age=3600.

Чтобы увидеть код до этого места, используйте тег step4-add-users в Git-репозитории.

Шаг 5. Развертывание приложения Chatter в Bluemix

Развернуть приложение в Bluemix очень легко. Чтобы упростить повторное развертывание приложения в Bluemix, создайте файл manifest.yml следующего содержания:

---
applications:
- name: TestChatter
  memory: 128M
  buildpack: https://github.com/cloudfoundry/cf-buildpack-python.git
  command: python serve.py

Он предписывает клиенту CF передать приложение с именем TestChatter, используя параметры 128M, Python buildpack и сценарий запуска serve.py.

Чтобы не передавать в Bluemix вместе с приложением ненужные файлы, создайте файл .cfignore следующего содержания:

Chatter.sqlite
dist

Обработка VCAP_APP_PORT

Чтобы приложение работало в Bluemix, оно должно прослушивать нужный порт. Добавьте в вызов serve в файле serve.py параметр port:

        serve(app, host='0.0.0.0', port=os.getenv('VCAP_APP_PORT', 6543))

Теперь войдите, передайте приложение в Bluemix и откройте его, используя URL-адрес приложения:

cf login -a https://api.ng.bluemix.net
cf push

Обработка VCAP_SERVICES

Если перезапустить приложение, журнал сообщений исчезнет. Чтобы он был постоянно доступен, используйте службы базы данных Bluemix. Измените способ инициализации механизма базы данных, заменив строку engine = engine_from_config(settings, 'sqlalchemy.') в файле chatter/__init__.py следующим кодом:

    engine = None
    import os
    if os.environ.has_key("VCAP_SERVICES"):
        import json
        services = json.loads(os.environ["VCAP_SERVICES"])
        for service in reduce(lambda x,y: x+y, services.values(), []):
            if service['name'] == 'messages':
                from sqlalchemy import create_engine
                engine = create_engine(service['credentials']['uri'], pool_recycle=3600)
    if not engine:
        engine = engine_from_config(settings, 'sqlalchemy.')

Этот код использует службу 'messages' для получения URI базы данных. Службы периодически закрываютдолгоживущие соединения. Для нормальной работы соединения нужно периодически возобновлять (закрывать и открывать снова). Метод pool_recycle=3600 перезапускает таким образом соединения, открытые более часа. Кроме URI, нужно добавить драйверы базы данных. Они добавляются в список requires[] в файле setup.py. Добавьте PostgreSQL с помощью строки psycopg2. Теперь приложение можно использовать с базами данных PostgreSQL:

cf create-service postgresql 100 messages
cf bind-service TestChatter messages
cf push

Обратите внимание, что данные теперь сохраняется даже после перезапуска приложения:

cf restart TestChatter

Добавление поддержки DB2

У DB2 нет клиента с открытым исходным кодом, так что нужно добавить необходимые библиотеки прямо в приложение. Загрузите пакет Data Server Driver Package for Linux/x 86-64 64 bit из Fix Packs for IBM Data Server Client Packages и поместите его в корневой каталог приложения. Даже если вы разрабатываете это приложение на другой платформе, используете загрузочный пакет для платформы Linux/x 86-64, так как в Bluemix приложение будет работать на ней. Версия драйвера v10.5 Fix Pack 5 находится в файле v10.5fp5_linuxx64_dsdriver.tar.gz.

Добавьте процесс установки пакетов DB2 в файл setup.py. Менеджер пакетов pip по умолчанию не обрабатывает предварительно упакованные файлы, так что используйте вместо него easy_install. Добавьте в файл setup.py перед вызовом setup() следующие строки:

if "-".join(os.uname()[0::4]) == "Linux-x86_64":
    from glob import glob
    from subprocess import call

    if not os.path.isdir('./dsdriver'):
        db2_drivers=glob("./v*_linuxx64_dsdriver.tar.gz")
        if db2_drivers:
            import tarfile
            print "Using %s" % db2_drivers[0]
            tar = tarfile.open(db2_drivers[0])
            tar.extractall()
            tar.close()
            call(["bash", "./dsdriver/installDSDriver"])

            for f in db2_drivers:
                os.remove(f)
            import shutil
            for d in os.listdir("./dsdriver"):
                if not d in ["lib", "python"]:
                    f = os.path.join("./dsdriver", d)
                    if os.path.isdir(f):
                        shutil.rmtree(f)
                    else:
                        os.remove(f)

    if os.path.isdir('./dsdriver/python/python64'):
        call(["easy_install", "-N", glob("./dsdriver/python/python64/ibm_db-*-linux-x86_64.egg")[0]])
        call(["easy_install", "-N", glob("./dsdriver/python/python64/ibm_db_sa-*.egg")[0]])

Обратите внимание, что:

  • установка происходит, только если ОС действительно та, для которой у вас есть драйвер. извлечение файлов выполняется, только если это не было сделано раньше;
  • параметр -N предписывает easy_install не проверять наличие в сети новых пакетов;
  • так как используется glob, код будет работать с любой версией пакетов;
  • разделение установки драйверов и установки пакетов Python на два отдельных условных перехода if позволяет пользователям, имеющим доступ к Linux, ускорить первоначальный запуск приложения, включив в приложение уже установленный драйвер;
  • удаление файлов в конце первого блока if уменьшает объем дискового пространства, занимаемого приложением в облаке.

Наконец, добавьте к пути загрузки библиотек собственную библиотеку DB2. Это делается в среде приложения, так что добавьте в файл manifest.yml следующую строку:

  env:
   LD_LIBRARY_PATH: /app/dsdriver/lib:$LD_LIBRARY_PATH

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

cf unbind-service TestChatter messages
cf delete-service -f messages
cf create-service sqldb sqldb_free messages
cf bind-service TestChatter messages
cf push

Чтобы увидеть код до этого места, используйте тег step5-bluemixify в Git-репозитории.

Заключение

В этом руководстве показано:

  • как создать приложение на базе Pyramid с моделью базы данных, не зависящей от типа базы данных;
  • как обеспечить динамическое обновление страницы с помощью библиотеки абстрагирования SocketIO;
  • как обеспечить аутентификацию пользователей приложения;
  • как развернуть приложение в среде Bluemix;
  • как использовать службу базы данных, доступную в Bluemix.

Представленное приложение составляет основу для создания Python-приложений в среде Bluemix.

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


Ресурсы для скачивания


Похожие темы


Комментарии

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Облачные вычисления
ArticleID=1017113
ArticleTitle=Создание чат-приложения с помощью Pyramid, SQLDB и Bluemix
publish-date=10082015