=======
 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.

.. image:: /_images/chat.png
   :width: 80%

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()
   
