Sprinkling in some server-side stuff

Alright, now that we’ve got a page working, let’s add some functionality.

Before we do that, though, add this class to the main.py file. It contains some multithreading and fileio stuff to make it easy to add and remove tasks.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
class TodoReader:
    def __init__(self):
        self.todos = {}
        self.last_todo = 0
        try:
            with open('todo.txt', 'r') as old:
                for i, line in enumerate(old.readlines()):
                    self.todos[i] = line
                    self.last_todo = max(self.last_todo, i)
        except IOError:
            pass
        self.add_queue = eventlet.queue.Queue()
        self.adds = {}
        self.remove_queue = eventlet.queue.Queue()

    def add_daemon(self):
        while True:
            add = self.add_queue.get()

            self.todos[self.last_todo + 1] = add
            self.adds[add].put(self.last_todo + 1)
            self.last_todo += 1

            with open('todo.txt', 'w') as new:
                for index in self.todos:
                    new.write(self.todos[index] + ("\n" if not self.todos[index].endswith("\n") else ""))

    def del_daemon(self):
        while True:
            d = self.remove_queue.get()

            del self.todos[d]
            self.last_todo = 0
            for i in self.todos:
                self.last_todo = max(self.last_todo, i)

            with open('todo.txt', 'w') as new:
                for index in self.todos:
                    new.write(self.todos[index] + ("\n" if not self.todos[index].endswith("\n") else ""))

    def start(self):
        pool = eventlet.greenpool.GreenPool(2)
        pool.spawn_n(self.add_daemon)
        pool.spawn_n(self.del_daemon)

    def add(self, text):
        self.add_queue.put(text)
        self.adds[text] = eventlet.queue.Queue()

    def wait_on(self, text):
        ind = self.adds[text].get()
        del self.adds[text]
        return ind

    def remove(self, index):
        self.remove_queue.put(index)

This should go somewhere after the creation of app instance

After that, but before the view code, add this to properly initialize it:

1
2
todo = TodoReader()
todo.start()

Alright, so now we have a variable called todo that manages... todos!

Showing the current todos and removing them

At the heart of server-side DOM manipulation in pycommunicate is HTMLWrapper. For all of the methods it supports, go look at it, but the one we will be using is element(). This method will return a ElementWrapper tracked to follow the selector given. This can then be used to modify the DOM.

Add a load() method

When a user loads a page with the pycommunicate libraries installed, as soon as document.ready() occurs client-side, the library will connect to the server and when the server finishes initializing the connection, the active view’s load() method is called.

Let’s create this method and add some code to it after the render() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def load(self):
    todo_div = self.html_wrapper.element("#todo")

    loading_message = self.html_wrapper.element("#loadingBar")
    loading_message.delete()

    for index in todo.todos:
        text = todo.todos[index]
        todo_page_div = todo_div.add_child("div")
        todo_page_div.id = "todo{}".format(index)
        text_p = todo_page_div.add_child("p")
        text_p.id = "todoText{}".format(index)
        text_p.content = text
        button = todo_page_div.add_child("button")
        button.id = "todo{}".format(index)
        button.add_event_listener("click", self.make_handler(index))
        button.content = "Remove"

After doing this, if you are using an IDE, it will complain about self.make_handler not existing, so make that too:

1
2
3
4
5
6
7
def make_handler(self, index):
     def handler():
         todo.remove(index)

         todo_div = self.html_wrapper.element("#todo{}".format(index))
         todo_div.delete()
     return handler

Alright, so lets explain what’s actually happening there

What’s going on in the load() function ?!

Alright, let’s do this bit by bit:

2
todo_div = self.html_wrapper.element("#todo")

The first part is probably self-explanatory to all python programmers, so let’s explain that call. As I said earlier the element() method returns a ElementWrapper. In this case, it is tracking the first thing with an id of todo. In our html file, that points to the <div>. So this call will set todo_div equal to something that represents... the todo div!

4
5
loading_message = self.html_wrapper.element("#loadingBar")
loading_message.delete()

The first line does similar things to the example above, so lets explain the second line. It calls loading_message‘s delete() method, which deletes the element. This effectively clears the “Loading...” message from the page.

 7
 8
 9
10
11
12
13
for index in todo.todos:
    text = todo.todos[index]
    todo_page_div = todo_div.add_child("div")
    todo_page_div.id = "todo{}".format(index)
    text_p = todo_page_div.add_child("p")
    text_p.id = "todoText{}".format(index)
    text_p.content = text

So the loop goes through every todo in the TodoReader. This class uses ids and a dictionary to store todos, so we loop through the keys, which are the indices.

Note

Although I could of used a list, this seemed easier to implement and keep track for removing entries, so I used a dictionary.

For each todo, get its text and store it in text. Then, use the element creation function add_child() to create and get a <div> element with id todo{index} inside the todo_div. The next call is very similar, only calling it on todo_page_div and using it to create a <p> element with id todoText{index} instead.

The content attribute is a wrapper for innerText, which can be used to get or set the text of an element. We use this to change the text of the new <p> element to the text of the todo.

Warning

The get functions of element properties block until the property is received, while the set() functions return as soon as the change is submitted to be sent. This means that calls to set() and then immediately after get() can return the wrong values. This will probably be changed in a later version, or an option added to block on the set() call.

12
13
14
15
button = todo_page_div.add_child("button")
button.id = "todo{}".format(index)
button.add_event_listener("click", self.make_handler(index))
button.content = "Remove"

Line 12 and 14 use already explained functions, so I’ll detail the add_event_listener() method instead. This method will attach an event to a js event. These are using the chrome and firefox names, not the IE ones. We use it here to attach the button’s click method to a dynamically generated event handler setup to destroy the todo server-side, and then use delete() to remove it from the client-side.

Adding todos

Add code to the load() method

To do this, you need to add the following lines at the end of load():

1
2
add_button = self.html_wrapper.element("#add")
add_button.add_event_listener("click", self.add_handler)

I’ve already explained above what this does, so let’s go create that add_handler event handler.

The add_handler method

The add_handler method will deal with when the user clicks the “Add” button. Here’s the code in it:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
text = "- " + self.html_wrapper.element("#next").get_property("value")
todo.add(text)
self.html_wrapper.element("#next").set_property("value", "")
index = todo.wait_on(text)
todo_div = self.html_wrapper.element("#todo")
todo_page_div = todo_div.add_child("div")
todo_page_div.id = "todo{}".format(index)
text_p = todo_page_div.add_child("p")
text_p.id = "todoText{}".format(index)
text_p.content = text
button = todo_page_div.add_child("button")
button.id = "todo{}".format(index)
button.add_event_listener("click", self.make_handler(index))
button.content = "Remove"

Lines 5-11 are simply copied from the load() function, so look there for info on what these do.

Again, I’ll go line by line.

1
text = "- " + self.html_wrapper.element("#next").get_property("value")

This will set text to “- ” plus whatever is in the input field. The get_property() method will return whatever the JS element defined by the ElementWrapper has for that name. The value property contains the content of the input field.

Line 3 simply empties it using set_property().

Lines 2 and 4 use the TodoReader to add and retrieve the index for the new entry, and the rest is just as above.

Putting it all together

Your main.py file should now look like this:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import eventlet

from pycommunicate.server.bases.views import View
from pycommunicate.server.bases.controller import ControllerFactory
from pycommunicate.server.app.communicate import CommunicateApp

app = CommunicateApp()

# This class uses some greenlet things and is beyond the scope of this tutorial


class TodoReader:
    def __init__(self):
        self.todos = {}
        self.last_todo = 0
        try:
            with open('todo.txt', 'r') as old:
                for i, line in enumerate(old.readlines()):
                    self.todos[i] = line
                    self.last_todo = max(self.last_todo, i)
        except IOError:
            pass
        self.add_queue = eventlet.queue.Queue()
        self.adds = {}
        self.remove_queue = eventlet.queue.Queue()

    def add_daemon(self):
        while True:
            add = self.add_queue.get()

            self.todos[self.last_todo + 1] = add
            self.adds[add].put(self.last_todo + 1)
            self.last_todo += 1

            with open('todo.txt', 'w') as new:
                for index in self.todos:
                    new.write(self.todos[index] + ("\n" if not self.todos[index].endswith("\n") else ""))

    def del_daemon(self):
        while True:
            d = self.remove_queue.get()

            del self.todos[d]
            self.last_todo = 0
            for i in self.todos:
                self.last_todo = max(self.last_todo, i)

            with open('todo.txt', 'w') as new:
                for index in self.todos:
                    new.write(self.todos[index] + ("\n" if not self.todos[index].endswith("\n") else ""))

    def start(self):
        pool = eventlet.greenpool.GreenPool(2)
        pool.spawn_n(self.add_daemon)
        pool.spawn_n(self.del_daemon)

    def add(self, text):
        self.add_queue.put(text)
        self.adds[text] = eventlet.queue.Queue()

    def wait_on(self, text):
        ind = self.adds[text].get()
        del self.adds[text]
        return ind

    def remove(self, index):
        self.remove_queue.put(index)

todo = TodoReader()
todo.start()  # start up the TodoReader.


class TodoView(View):
    def render(self):
        return self.controller.templater.render("home.html")

    def make_handler(self, index):
        def handler():
            todo.remove(index)

            todo_div = self.html_wrapper.element("#todo{}".format(index))
            todo_div.delete()
        return handler

    def add_handler(self):
        text = "- " + self.html_wrapper.element("#next").get_property("value")
        todo.add(text)
        self.html_wrapper.element("#next").set_property("value", "")
        index = todo.wait_on(text)
        todo_div = self.html_wrapper.element("#todo")
        todo_page_div = todo_div.add_child("div")
        todo_page_div.id = "todo{}".format(index)
        text_p = todo_page_div.add_child("p")
        text_p.id = "todoText{}".format(index)
        text_p.content = text
        button = todo_page_div.add_child("button")
        button.id = "todo{}".format(index)
        button.add_event_listener("click", self.make_handler(index))
        button.content = "Remove"

    def load(self):
        # add existing todos:
        todo_div = self.html_wrapper.element("#todo")

        loading_message = self.html_wrapper.element("#loadingBar")
        loading_message.delete()

        for index in todo.todos:
            text = todo.todos[index]
            todo_page_div = todo_div.add_child("div")
            todo_page_div.id = "todo{}".format(index)
            text_p = todo_page_div.add_child("p")
            text_p.id = "todoText{}".format(index)
            text_p.content = text
            button = todo_page_div.add_child("button")
            button.id = "todo{}".format(index)
            button.add_event_listener("click", self.make_handler(index))
            button.content = "Remove"

        add_button = self.html_wrapper.element("#add")
        add_button.add_event_listener("click", self.add_handler)

controller = ControllerFactory().add_view(TodoView).set_default_view(TodoView)
app.add_controller("/", controller)

app.set_secret_key("todo_secrets!")
app.run()

If it looks like that (give or take some whitespace or comments) then you’re good to go! Simply run it and connect to it with the link in the console and watch your creation work!!

This is the end of the tutorial, but I’m sure you could do other stuff with this if you want.