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.
Models are by convention UpperCamelCase singular nouns. Examples include Bear, Dalek, BlogPost, InfoPage.
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.
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.
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 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()))
You will need to login to the application first by opening http://localhost:8080/_ah/login. Otherwise users.get_current_user will return None.
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:
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 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.
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('email@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('firstname.lastname@example.org') # 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.
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