Modeling Data

First and foremost, we need a way to store our data. Ferris extends 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 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 the ordinary ferris.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 UpperCamelCase singular nouns. Examples include Bear, Dalek, BlogPost, InfoPage.

Experimenting with your model

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:8000/ 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 post2.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. Be sure to restart the interactive console instance after making modifications.

Required fields

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.

Querying

Ferris models are 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()))

Note

You will need to login to the application first by opening http://localhost:8080/_ah/login. Otherwise users.get_current_user will return None.

Note

The datastore is eventually consistent outside of entity groups so you may not see the new entities in queries immediately. You can run this example line-by-line in the interactive console if you don’t see stuff in your query.

Our requirements call for the following queries on Posts:

  • All posts in descending date order.
  • Posts from a given user in descending date order.

Create these queries as methods on the Post class. Any and all consumers of Post 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 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 descending.
        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.

Testing your model

All of the tests for your application reside inside of app/tests and its subfolders. 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 nosetests --with-ferris app/tests.

Note

Windows users or users with a non-standard install will have to provide the --gae-sdk-path argument to nosetests with the path to your Google App Engine SDK. For more help see the Testing page.

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