Demos

The demo/ directory in FullPy sources includes several demo web apps.

Multi-room chat

This demo web app is a multi-room, multi-user chat system. It uses:

  • WebSocket, allowing the server to call the client(s) when new messages are available,

  • Session, allowing the server to store per-client data (as required by WebSocket),

  • Group, each group representing a chat room, associated with one or more user sessions,

  • Ontology quadstore, for data persistence, including chat messages but also sessions and groups.

_images/chat.png

Server

First, the server app enables GEvent (mandatory for WebSocket) and import GEvent, FullPy and Owlready among other modules:

from gevent import monkey
monkey.patch_all()

import sys, os, os.path, datetime, gevent
from owlready2 import *
from fullpy.server import *

Second, we create a new World for storing persistent data. Data is stored in /tmp/demo_chat.sqlite3 (you may need to adapt the filename according to your system).

We load the FullPy ontology (which define a few classes, like User, Session and Group), and create a new ontology “chat.owl”.

world       = World(filename = "/tmp/demo_chat.sqlite3")
fullpy_onto = get_fullpy_onto(world)
chat_onto   = world.get_ontology("http://test.org/chat.owl")

Third, within the chat ontology, we subclass User, Session and Group.

The Group subclass is named ChatRoom, it has an internal name (name attribute, automatically generated by Owlready) and a user-flrendly label (label attribute).

We also create a simple object model for chat:
  • the Message class correspond to a message in the chat, the as_tuple() method is used to serialize a message as a tuple,

  • the text data property indicate the text of a message,

  • the date data property indicate the date of a message,

  • the messages object property indicate the list of messages associated with a given Group.

with chat_onto:
  class MyUser(fullpy_onto.User): pass

  class MySession(fullpy_onto.Session): pass

  class ChatRoom(fullpy_onto.Group): pass

  class Message(Thing):
    def as_tuple(self):
      return ("%s/%s/%s %s:%s" % (self.date.day, self.date.month, self.date.year,
                                  self.date.hour, self.date.minute), self.user.login, self.text)

  class text(Message >> str, FunctionalProperty): pass
  class date(Message >> datetime.datetime, FunctionalProperty): pass
  class messages(ChatRoom >> Message): pass

Fourth, we create 3 users and 3 chat rooms:

chat_onto.MyUser(login = "user1", password = "123")
chat_onto.MyUser(login = "user2", password = "123")
chat_onto.MyUser(login = "user3", password = "123")

chat_onto.ChatRoom(label = ["Python programming"])
chat_onto.ChatRoom(label = ["Bird watching"])
chat_onto.ChatRoom(label = ["Bike riding"])

Fifth, we create the web app server class by subclassing ServerSideWebapp.

In __init__() we call use_python_client(), use_ontology_quadstore(), use_session() and use_websocket().

class MyWebApp(ServerSideWebapp):
  def __init__(self):
    ServerSideWebapp.__init__(self)
    self.name          = "demo_4"
    self.title         = "FullPy demo"
    self.url           = "/index.html"
    self.static_folder = os.path.join(os.path.dirname(__file__), "..", "static")
    self.js            = []
    self.css           = ["demo_4.css"]

    self.use_python_client(os.path.join(os.path.dirname(__file__), "client.py"))
    self.use_ontology_quadstore(world)
    self.use_session(chat_onto.MySession, chat_onto.ChatRoom)
    self.use_websocket(debug = True)

Sixth, we define four remotely-callable methods:

  • server_get_chat_room_names(): returns a dict mapping chat room names to their labels.

@rpc
def server_get_chat_room_names(self, session):
  return { chat_room.name : chat_room.label.first() for chat_room in ChatRoom.instances() }
  • server_join_chat_room(): joins the given chat room (indicated by its name).

    The current client (identified by its session) joins the given chat room (and implicitely leaves the current one). This method calls session.join_group(). It returns the list of messages in the chat room.

@rpc
def server_join_chat_room(self, session, chat_room_name):
  if session.groups: session.quit_group(session.groups[0])
  chat_room = chat_onto[chat_room_name]
  session.join_group(chat_room)
  return [message.as_tuple() for message in sorted(chat_room.messages, key = lambda message: message.date)]
  • server_create_chat_room(): creates a new chat room.

    This method create a new instance of the ChatRoom class. The new chat room is automatically stored in the ontology quadstore. Finally, it calls client_new_chat_room() for all clients, in order to inform them of the existence of the new chat room.

@rpc
def server_create_chat_room(self, session, chat_room_label):
  with chat_onto:
    chat_room = chat_onto.ChatRoom(label = chat_room_label)
  self.client_new_chat_room(None, chat_room.name, chat_room_label)
  • server_add_message(): adds a new message in the current session’s chat room.

    This method create a new instance of the Message class, and add it to the session’s current chat room. Finally, it calls client_new_message() for all client in the chat room (remember that ChatRoom inherit from Group, and client_xxx() remote functions can be called on a Group).

@rpc
 def server_add_message(self, session, text):
   chat_room = session.groups[0]
   with chat_onto:
     message = chat_onto.Message(date = datetime.datetime.now(), user = session.user, text = text)
     chat_room.messages.append(message)
   chat_room.client_new_message(None, *message.as_tuple())

Seventh, we run the web app with GUnicorn:

from fullpy.server.gunicorn_backend import *
serve_forever([MyWebApp()], "http://127.0.0.1:5000")

Client

First, we import Fullpy:

from fullpy.client import *
from fullpy.client.auth import *

Second, we create the client web app by subclassing ClientSideWebapp. In __init__(), we create two attributes, chat_room (the name of the current chat room) and messages (the list of messages in the current chat room).

class MyWebApp(ClientSideWebapp):
  def __init__(self):
    ClientSideWebapp.__init__(self)
    self.chat_room = None
    self.messages  = []

Third, we override on_session_opened(). This methods is called when a new session is opened (anonymous or authentified).

If the session is anonymous (i.e. user_login is empty), we show a LoginDialog to prompt user for login.

Otherwise, we call server_get_chat_room_names() on the server, in order to obtain the dict of available chat rooms. When the dict is obtained, done() is called back, and we store the dict and call select_chat_room() to select by default the first chat room.

def on_session_opened(self, user_login, user_class, client_data):
  if not user_login:
    LoginDialog(None).show_popup()
    return

  def done(chat_room_names):
    self.chat_room_names = chat_room_names
    self.select_chat_room(sorted(chat_room_names)[0])
  self.server_get_chat_room_names(done)

Fourth, we define the select_chat_room() method. It calls server_join_chat_room() on the server and, when done, it stores the chat room name, the current messages returned by the sever, and call create_html().

def select_chat_room(self, name):
  def done(messages):
    self.chat_room = name
    self.messages  = messages
    self.create_html()
  self.server_join_chat_room(done, name)

Fifth, we define the create_html() method. It creates the main HTML interface, which is composed of 3 HTML widgets (chat room list on the left, message view on the right, and entry box at the bottom right). It assembles the 3 widgets inside an HTML table, and shows it.

def create_html(self):
  self.chat_room_list = ChatRoomList()
  self.message_view   = MessageView()
  self.entry_box      = EntryBox()

  self.main_html = HTML()
  self.main_html << """<table id="chat_table" cellspacing="0"><tr><td>"""
  self.main_html << self.chat_room_list
  self.main_html << """</td><td>"""
  self.main_html << self.message_view
  self.main_html << self.entry_box
  self.main_html << """</td></tr></table>"""
  self.main_html.show()

Sixth, we define the client_new_chat_room() remotely-callable method. It is called by the server whenever a new chat room has been created. In that case, we update the dict of chat room names, and we refresh the chat room list HTML widget.

@rpc
def client_new_chat_room(self, chat_room_name, chat_room_label):
  webapp.chat_room_names[chat_room_name] = chat_room_label
  webapp.chat_room_list.refresh()

Seventh, we define the client_new_message() remotely-callable method. It is called by the server whenever a new message is available in the current chat room. In that case, we add the message to the list of current messages, and we call add_message() on the message view HTML widget.

@rpc
def client_new_message(self, date, user_login, message_text):
  webapp.messages.append((date, user_login, message_text))
  webapp.message_view.add_message(date, user_login, message_text)

The ChatRoomList HTML widget shows the list of available chat rooms. It also has a “Create new room…” button. When clicked, it creates and shows an instance of the NewRoomDialog HTML widget.

class ChatRoomList(HTML):
  def build(self, builder):
    self << """<div id="chat_room_list"><div class="title">FullPy Chat rooms :</div>"""
    for name, label in sorted(webapp.chat_room_names.items(), key = lambda i: i[1]):
      if name == webapp.chat_room:
        self << """<div id="chat_room_%s" class="chat_room selected">%s</div>""" % (name, label)
      else:
        self << """<div id="chat_room_%s" class="chat_room">%s</div>""" % (name, label)
      def on_click(event, name = name):
        webapp.select_chat_room(name)
      self.bind("chat_room_%s" % name, "click", on_click)
    self << """<input id="new_room" type="button" value="Create new room..."></input>"""
    self.bind("new_room", "click", self.on_new_room)
    self << """</div>"""

  def on_new_room(self, event): NewRoomDialog().show_popup()

  def refresh(self): self.show_replace("chat_room_list")

The NewRoomDialog HTML widget ask the user for the label of the new room. If the “Ok” button is clicked, it calls the server_create_chat_room() remotely-callable function.

class NewRoomDialog(HTML):
  def build(self, builder):
    self << """<h2>Create new chat room :</h2>"""
    self << """Room label : <input id="chat_room_label" type="text"></input><br/><br/>"""
    self << """<input id="ok" type="button" value="Ok"></input>"""
    self.bind("ok", "click", self.on_ok)

  def on_ok(self, event):
    chat_room_label = document["chat_room_label"].value.strip()
    webapp.server_create_chat_room(None, chat_room_label)
    hide_popup()

The MessageView HTML widget show the list of messages. It has a add_message() method, that is used to add new message.

class MessageView(HTML):
  def build(self, builder):
    self << """<div id="message_view">"""
    if webapp.messages:
      for date, user_login, message_text in webapp.messages:
        self << self.message_to_html(date, user_login, message_text)
    self << """</div>"""

  def message_to_html(self, date, user_login, message_text):
    if user_login == webapp.user_login:
      html = """<div class="message self">"""
    else:
      html = """<div class="message">"""
    html += """<div class="message_header">%s (%s) :</div>""" % (user_login, date)
    html += """<div class="message_content">%s</div></div>""" % message_text
    return html

  def add_message(self, date, user_login, message_text):
    document["message_view"].insertAdjacentHTML("beforeend", self.message_to_html(date, user_login, message_text))

The EntryBox HTML widget is an input field that can be used to enter new messages. When a new message is entered, it call the server_add_message() remotely-callable function.

class EntryBox(HTML):
  def build(self, builder):
    self << """<div id="entry_box">"""
    self << """<table id="entry_table"><tr><td>Say&nbsp;something:</td>"""
    self << """<td id="entry_td"><input id="entry" type="text"></input></td>"""
    self << """<td><input id="send" type="button" value="Send"></input></td></tr></table>"""
    self << """</div>"""
    self.bind("entry", "keypress", self.on_keypress)
    self.bind("send",  "click",    self.on_send)

  def on_keypress(self, event):
    if event.key == "Enter": self.on_send(event)

  def on_send(self, event):
    text = document["entry"].value.strip()
    if text:
      webapp.server_add_message(None, text)

Finally, we instanciate the web app:

MyWebApp()