First and foremost we need a way to store our data. Ferris uses the native Google App Engine Datastore and the ndb module. This section will walk you through creating a Model and how to interact with it.
Models reside inside of app/models, let’s create our Post model at app/models/post.py:
from ferris.core.ndb import BasicModel
from google.appengine.ext import ndb
class Post(BasicModel):
title = ndb.StringProperty()
content = ndb.TextProperty()
This is a very simple model with only two fields: the title of the post, and the post’s content. You may say, “wait, we also need to know who created that post and when!” You would be right, which is why we used BasicModel. BasicModel extends from ferris.core.ndb.Model and provides us with four automatic fields:
created = ndb.DateTimeProperty(auto_now_add=True)
created_by = ndb.UserProperty(auto_current_user_add=True)
modified = ndb.DateTimeProperty(auto_now=True)
modified_by = ndb.UserProperty(auto_current_user=True)
Since these fields are often necessary they’re provided as part of the core framework.
Note
Models are by convention singular nouns.
At this point we have a model, so let’s get a feel for how to interact with it.
The App Engine Development Server has an excellent feature: the Interactive Console. Open http://localhost:8080/_ah/admin in your browser and click on ‘Interactive Console’ in the sidebar.
Enter this into the Interactive Console:
from app.models.post import Post
# Create a new post
post = Post(title="Test Post", content="A basic post")
# Save the post, it doesn't exist in the datastore until put() is called
post.put()
print post.title
print post.content
# Modify the title, and save it again.
post.title = "A new title"
post.put()
# Get a fresh copy
post2 = post.key.get()
print post.title
# Delete the post, we no longer need it.
post.key.delete()
This snippet walks you through the basics of interacting with a model: creating, reading, updating, and deleting.
Note
Most of the examples below should be entered into the interactive console unless otherwise stated.
Often it’s desirable to make sure a field is present before allowing an entity to be saved. This is quite easy.
Let’s modify our Post class:
class Post(BasicModel):
title = ndb.StringProperty(required=True)
content = ndb.TextProperty()
Let’s create a post without a title:
from app.models.post import Post
post = Post(content="A basic post")
# Raises a BadValueError
post.put()
Ferris will recognize required fields when building forms. If a required field is omitted, the end user will see a nicely formatted validation error.
Ferris models are google.appengine.ext.ndb.Model subclasses, so you can use any and all methods of querying ordinary models.
However, Ferris does provide you with some shortcuts:
from app.models.post import Post
from google.appengine.api import users
# create some posts
Post(title="Post One", content="...").put()
Post(title="Post Two", content="...").put()
# Find just that first post.
print Post.find_by_title("Post One").title
# Find all posts by the current user.
print list(Post.find_all_by_created_by(users.get_current_user()))
Our requirements call for the following queries on Posts:
Create these queries as methods on the Posts class. Any and all consumers of Posts will use the queries defined in the class method, making it easy to adjust the query for all consumers. This is the “fat model” approach.
Here’s our modified Post model with these query methods:
from ferris.core.ndb import BasicModel
from google.appengine.ext import ndb
from google.appengine.api import users
class Post(BasicModel):
title = ndb.StringProperty(required=True)
content = ndb.TextProperty()
@classmethod
def all_posts(cls):
"""
Queries all posts in the system, regardless of user, ordered by date created descending.
"""
return cls.query().order(-cls.created)
@classmethod
def all_posts_by_user(cls, user=None):
"""
Queries all posts in the system for a particular user, ordered by date created desceding.
If no user is provided, it returns the posts for the current user.
"""
if not user:
user = users.get_current_user()
return cls.find_all_by_created_by(user).order(-cls.created)
Now you can use Post.all_posts() and Post.all_posts_by_user() to execute these queries.
All of the tests for your application reside inside of app/tests/backend. We’re going to create a test to ensure that our model’s query methods do exactly as we expect.
Create the following file in app/tests/backend/test_post.py:
from ferris.tests.lib import WithTestBed
from app.models.post import Post
class TestPost(WithTestBed):
def testQueries(self):
# log in user one
self.loginUser('user1@example.com')
# create two posts
post1 = Post(title="Post 1")
post1.put()
post2 = Post(title="Post 2")
post2.put()
# log in user two
self.loginUser('user2@example.com')
# create two more posts
post3 = Post(title="Post 3")
post3.put()
post4 = Post(title="Post 4")
post4.put()
# Get all posts
all_posts = list(Post.all_posts())
# Make sure there are 4 posts in total
assert len(all_posts) == 4
# Make sure they're in the right order
assert all_posts == [post4, post3, post2, post1]
# Make sure we only get two for user2, and that they're the right posts
user2_posts = list(Post.all_posts_by_user())
assert len(user2_posts) == 2
assert user2_posts == [post4, post3]
This test is lengthy but it adequately covers the functionality we require from our data model.
We can continue with the confidence that our data model and its queries are sound. To run these tests, execute scripts/backend_test.sh or alternatively python ferris/scripts/test_runner.py app.
Note
Windows users or users with a non-standard install will have to provide the -sdk argument to the test runner with the path to your Google App Engine SDK.
Your output should resemble this:
testQueries (app.tests.backend.test_post.TestPost) ... ok
testRoot (app.tests.backend.test_sanity.SanityTest) ... ok
testRoot (app.tests.test_sanity.SanityTest) ... ok
----------------------------------------------------------------------
Ran 3 tests in 0.298s
OK
Continue with Handling Requests