Part 2: Adding Features
Now that we have a working development environment and have become familiar with running our todo app, we can build in our initial feature set to manage task items.

Persistent Storage

Before we can create anything, we need some way to store it. For this project, we've chosen TinyDB, a light-weight key-value database that stores data on disk in a single JSON file.
Let's begin by adding the dependency to our requirements.txt file, and installing it to our virtualenv:
Add TinyDB Dependency
Add the following to the bottom of the pip requirements.txt file:
1
tinydb
Copied!
Install the new requirements with pip:
1
$ pip install -r requirements.txt
2
...
3
Successfully installed tinydb-3.10.0
Copied!
With our dependency installed, we need to add it to our application. The primary things we will cover here are:
    Configuration settings for where we will store the db.json file on disk
    Using framework hooks to run code at a specific point in our runtime
    Extending our app with a db object we will use to integrate and access the TinyDB functionality in our application
Add Configuration Defaults
Find and modify the following section of todo/main.py in order to define a default configuration for our database file called db_file:
1
# configuration defaults
2
CONFIG = init_defaults('todo')
3
CONFIG['todo']['db_file'] = '~/.todo/db.json'
Copied!
To be kind to our users, we will also want to add this default setting to our example configuration file config/todo.yml.example. Modify the file to include the following:
1
---
2
todo:
3
4
### Database file path
5
# db_file: ~/.todo/db.json
Copied!
We want to extend our application with a re-usable db object that can be used throughout our code. There are many ways we could do this, however here we are going to use a framework hook.
Add DB Object Code
Add the following to the top of the todo/main.py file:
1
import os
2
from tinydb import TinyDB
3
from cement.utils import fs
4
5
def extend_tinydb(app):
6
app.log.info('extending todo application with tinydb')
7
db_file = app.config.get('todo', 'db_file')
8
9
# ensure that we expand the full path
10
db_file = fs.abspath(db_file)
11
app.log.info('tinydb database file is: %s' % db_file)
12
13
# ensure our parent directory exists
14
db_dir = os.path.dirname(db_file)
15
if not os.path.exists(db_dir):
16
os.makedirs(db_dir)
17
18
app.extend('db', TinyDB(db_file))
Copied!
We've created a function to extend our application to include an app.db object, however in order for it to take affect we need to register the function as a hook with the framework. Add the following hooks meta option to our Todo app in todo/main.py:
1
class Todo(App):
2
class Meta:
3
hooks = [
4
('post_setup', extend_tinydb),
5
]
Copied!
Now, when we run todo again you will see that our hook is executed (via the info logs):
1
$ todo --help
2
INFO: extending todo application with tinydb
3
INFO: tinydb database file is: /Users/derks/.todo/db.json
4
...
Copied!
And we can see that the database was created:
1
$ cat ~/.todo/db.json
2
{"_default": {}}
Copied!

Controllers and Sub-Commands

In order to work with todo items we need to map out commands with our app. We could do this with the existing Base controller, however to keep code clean and organized we want to create an new controller called Items.
At this point, we have a decision to make regarding controller stacking. Do we want our controllers commands to appear embedded under the primary applications namespace (ex: todo my-command) or do we want a separate nested namespace (ex: todo items my-command). As our application is still small, we will opt to embed our controllers commands under the primary namespace (to keep our commands and examples shorter).
Add Items Controller Code
Add the following stubs to todo/controllers/items.py as a placeholder for our sub-commands:
1
from cement import Controller, ex
2
3
4
class Items(Controller):
5
class Meta:
6
label = 'items'
7
stacked_type = 'embedded'
8
stacked_on = 'base'
9
10
@ex(help='list items')
11
def list(self):
12
pass
13
14
@ex(help='create new item')
15
def create(self):
16
pass
17
18
@ex(help='update an existing item')
19
def update(self):
20
pass
21
22
@ex(help='delete an item')
23
def delete(self):
24
pass
25
26
@ex(help='complete an item')
27
def complete(self):
28
pass
29
Copied!
We've created the controller code, however for it to take affect we need to register it with our application.
Register Controller with App
Add/modify the following in todo/main.py:
1
from .controllers.items import Items
2
3
class Todo(App):
4
class Meta:
5
# ...
6
handlers = [
7
Base,
8
Items,
9
]
Copied!
With our new controller registered, lets see it in action:
1
$ todo --help
2
INFO: extending todo application with tinydb
3
INFO: tinydb database file is: /Users/derks/.todo/db.json
4
usage: todo [-h] [-d] [-q] [-v]
5
{complete,create,delete,update} ...
6
7
A Simple TODO Application
8
9
optional arguments:
10
-h, --help show this help message and exit
11
-d, --debug full application debug mode
12
-q, --quiet suppress all console output
13
-v, --version show program's version number and exit
14
15
sub-commands:
16
{complete,create,delete,update}
17
complete complete an item
18
create create new item
19
delete delete an item
20
update update an existing item
21
22
Usage: todo command1 --foo bar
Copied!

Feature Functionality

We've stubbed out our Items controller and sub-commands, so lets add the actual code that will support each of these features:

Create Items

Add Create Items Code
Add/modify the following in todo/controllers/items.py:
todo/controllers/items.py
1
from time import strftime
2
3
class Items(Controller):
4
# ...
5
6
@ex(
7
help='create an item',
8
arguments=[
9
( ['item_text'],
10
{'help': 'todo item text',
11
'action': 'store' } )
12
],
13
)
14
def create(self):
15
text = self.app.pargs.item_text
16
now = strftime("%Y-%m-%d %H:%M:%S")
17
self.app.log.info('creating todo item: %s' % text)
18
19
item = {
20
'timestamp': now,
21
'state': 'pending',
22
'text': text,
23
}
24
25
self.app.db.insert(item)
Copied!
We've now built out the functionality to create items in our database, that will include the text, a state (pending/complete), and also the timestamp of when it was created/updated. Notice that we've added arguments to the sub-command function, and not the controller because the item_text argument is only relevant to the create action, and not the application or controller namespace as a whole.
Let's try it out:
1
$ todo create "Call Saul"
2
INFO: creating todo item: Call Saul
3
4
$ todo create "Go to Car Wash"
5
INFO: creating todo item: Go to Car Wash
6
7
$ todo create "Meet with Jessie About a Thing"
8
INFO: creating todo item: Meet with Jessie About a Thing
Copied!

List Items

We've created an item, so now we need to be able to list them. First, let's take a look at our database:
1
$ cat ~/.todo/db.json | python -m json.tool
2
{
3
"_default": {
4
"1": {
5
"timestamp": "2018-07-30 15:11:53",
6
"state": "pending",
7
"text": "Call Saul"
8
},
9
"2": {
10
"timestamp": "2018-07-30 15:12:07",
11
"state": "pending",
12
"text": "Go to Car Wash"
13
},
14
"3": {
15
"timestamp": "2018-07-30 15:12:54",
16
"state": "pending",
17
"text": "Meet with Jessie About a Thing"
18
}
19
}
20
}
Copied!
We can see that TinyDB automatically generates database IDs, so we will want to display that when listing our items so that we can easily update/delete/complete by ID later.
Add List Items Code
Add/modify the following in todo/controllers/items.py:
todo/controllers/items.py
1
class Items(Controller):
2
# ...
3
4
@ex(help='list items')
5
def list(self):
6
data = {}
7
data['items'] = self.app.db.all()
8
self.app.render(data, 'items/list.jinja2')
Copied!
Here we are pulling all of the items from the database, putting it into a data dictionary, then rendering with the Jinja2OutputHandler. Put the following in the template file todo/templates/items/list.jinja2:
todo/templates/items/list.jinja2
1
{% for item in items %}
2
{{ item.doc_id }} [{% if item.state == 'complete' %}X{% else %} {% endif %}] {{ item.text }}
3
{% endfor %}
Copied!
It's a little messy, but that's why we put this in a separate template and not in our code. We are including the ID so that we can use that for updating/deleting/etc, and also a [ ] (checkbox) that will be "checked" when the item's state is complete.
Let's have a go:
1
$ todo list
2
1 [ ] Call Saul
3
2 [ ] Go to Car Wash
4
3 [ ] Meet with Jessie About a Thing
Copied!

Update Items

If we've made a typo, or want to change an existing item we need a way to update it.
Add Update Items Code
Add/modify the following in todo/controllers/items.py:
todo/controllers/items.py
1
class Items(Controller):
2
# ...
3
4
@ex(
5
help='update an existing item',
6
arguments=[
7
( ['item_id'],
8
{'help': 'todo item database id',
9
'action': 'store' } ),
10
( ['--text'],
11
{'help': 'todo item text',
12
'action': 'store' ,
13
'dest': 'item_text' } ),
14
],
15
)
16
def update(self):
17
id = int(self.app.pargs.item_id)
18
text = self.app.pargs.item_text
19
now = strftime("%Y-%m-%d %H:%M:%S")
20
self.app.log.info('updating todo item: %s - %s' % (id, text))
21
22
item = {
23
'timestamp': now,
24
'text': text,
25
}
26
27
self.app.db.update(item, doc_ids=[id])
Copied!
Given a TinyDB ID, we can update our item including touching the timestamp and modifying the text. Let's update our todo item:
1
$ todo update 2 --text "Send Skyler to Car Wash"
2
INFO: updating todo item: 2 - Send Skyler to Car Wash
3
4
$ todo list
5
1 [ ] Call Saul
6
2 [ ] Send Skyler to Car Wash
7
3 [ ] Meet with Jessie About a Thing
Copied!

Complete Items

A TODO list is not complete (ah! pun intended) without the ability to check off items that are done. This operation gets a little more interesting as we want to also send an email message when items are completed.
Add Complete Items Code
Add/modify the following in todo/controllers/items.py:
todo/controllers/items.py
1
class Items(Controller):
2
# ...
3
4
@ex(
5
help='complete an item',
6
arguments=[
7
( ['item_id'],
8
{'help': 'todo item database id',
9
'action': 'store' } ),
10
],
11
)
12
def complete(self):
13
id = int(self.app.pargs.item_id)
14
now = strftime("%Y-%m-%d %H:%M:%S")
15
item = self.app.db.get(doc_id=id)
16
item['timestamp'] = now
17
item['state'] = 'complete'
18
19
self.app.log.info('completing todo item: %s - %s' % (id, item['text']))
20
self.app.db.update(item, doc_ids=[id])
21
22
### send an email message
23
24
msg = """
25
Congratulations! The following item has been completed:
26
27
%s - %s
28
""" % (id, item['text'])
29
30
self.app.mail.send(msg,
31
subject='TODO Item Complete',
32
to=[self.app.config.get('todo', 'email')],
33
from_addr='[email protected]',
34
)
Copied!
Add/modify the following in todo/main.py:
1
# configuration defaults
2
CONFIG = init_defaults('todo')
3
CONFIG['todo']['email'] = '[email protected]'
Copied!
Now let's complete one of our items:
1
$ todo complete 2
2
INFO: completing todo item id: 2
3
4
=============================================================================
5
DUMMY MAIL MESSAGE
6
-----------------------------------------------------------------------------
7
10
CC:
11
BCC:
12
Subject: TODO Item Complete
13
14
---
15
16
17
Congratulations! The following item has been completed:
18
19
2 - Send Skyler to Car Wash
20
21
22
-----------------------------------------------------------------------------
23
24
$ todo list
25
1 [ ] Call Saul
26
2 [X] Send Skyler to Car Wash
27
3 [ ] Meet with Jessie About a Thing
Copied!
Notice that the email message was not sent, but rather printed to console. This is because the default mail_handler is set to dummy. You can override this to use the smtp mail handler via the applications configuration files (ex:~/.todo.yml)

Delete Items

Finally, if we just want to get rid of something, we need the ability to delete it:
Add Delete Code
Add/modify the following in todo/controllers/items.py:
todo/controllers/items.py
1
class Items(Controller):
2
# ...
3
4
@ex(
5
help='delete an item',
6
arguments=[
7
( ['item_id'],
8
{'help': 'todo item database id',
9
'action': 'store' } ),
10
],
11
)
12
def delete(self):
13
id = int(self.app.pargs.item_id)
14
self.app.log.info('deleting todo item id: %s' % id)
15
self.app.db.remove(doc_ids=[id])
Copied!
And lets delete our completed item:
1
$ todo delete 2
2
INFO: deleting todo item id: 2
3
4
$ todo list
5
1 [ ] Call Saul
6
3 [ ] Meet with Jessie About a Thing
Copied!

Conclusion

That concludes Part 2! We now have a fully functional TODO application. In the next parts we will discuss more indepth about extending the project with plugins, and digging deeper on things like documentation and testing.
Last modified 3yr ago