Modeling Data

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.

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

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 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:

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

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.

Testing your model

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

Table Of Contents

Previous topic

Multi-User Blog

Next topic

Handling Requests

This Page