I am exploring the use of CherryPy as back-end and emberjs as front-end for a simple web-application that manages a list of books. Cherrypy simply serves a handlebars template upon a request for index:
import os
import cherrypy
from google.appengine.api import users
from google.appengine.ext import ndb
class Root:
def __init__(self):
# book REST API
self.books = BookAPI()
@cherrypy.expose
def index(self):
return open(os.path.join(template_env, 'index.hbs'))
And I use classes BooksAPI and Books to serve as RESTfull API that employs the Google data storage for storing book objects (I only store isbn now).
class BookAPI():
exposed=True
@cherrypy.tools.json_out()
def GET(self, isbn=None):
# get the current user
user = users.get_current_user()
if(isbn is None):
# query all books current user
ancestor_key = ndb.Key("Library", str(user.user_id()))
books = Book.query_books(ancestor_key).fetch(20)
# convert to JSON list of books
book_list = []
for index, b in enumerate(books):
book_list.append({'id': index, 'isbn': b.isbn})
result = {
"books": book_list
}
return result
def POST(self, isbn):
# get the current user
user = users.get_current_user()
# create book and save in data storage
parent_key = ndb.Key('Library', user.user_id())
book = Book(parent=parent_key, isbn=isbn)
book.put()
...
class Book(ndb.Model):
isbn = ndb.StringProperty()
@classmethod
def query_books(cls, ancestor_key):
return cls.query(ancestor=ancestor_key)
For the emberjs clientside I use the RESTAdapter:
window.Books = Ember.Application.create();
Books.ApplicationAdapter = DS.RESTAdapter.extend();
My emberjs book model is defined as follows:
Books.Book = DS.Model.extend({
isbn: DS.attr('string'),
});
And I added the following book controllers:
Books.BookController = Ember.ObjectController.extend({
actions: {
removeBook: function() {
var book = this.get('model');
book.deleteRecord();
book.save();
}
}
});
Books.BooksController = Ember.ArrayController.extend({
actions: {
createBook: function() {
// get book isbn
var isbn = this.get('newIsbn');
if(!isbn.trim()) { return; }
// create new book model
var book = this.store.createRecord('book', {
isbn: isbn,
});
// clear the 'new book' text field
this.set('newIsbn', '');
// Save model
book.save();
}
}
});
And finally the following routes:
Books.Router.map(function () {
this.resource('books', { path: '/' });
});
Books.BooksRoute = Ember.Route.extend({
model: function() {
return this.store.find('book');
}
});
Adding and deleting books using the FixedAdapter worked, then I switched to the RESTAdapter.
The GET method worked. Emberjs automatically invokes the GET method and successfully obtains a list of books in JSON format that are displayed in the index.hbs template.
However, emberjs calls the POST method in a way I did not expect. It seems that ember sends an empty POST, without the isbn added as POST data. Because when I remove the isbn keyword argument from the cherrypy POST function, the function does get called. I need the isbn though, to create the book object.
I am probably forgetting something obvious here, but I cannot figure out what. Does anyone has an idea what I am forgetting or doing wrong? Thanks.
Bastiaan
For saving new records Ember send a json representation of the object being saved in the post body...
In your case sholud be
book:{isbn:[the isbn value]}
So there is no isbn parameter
Can you test with this on your post function
def POST(self):
# get the current user
user = users.get_current_user()
cl = cherrypy.request.headers['Content-Length']
rawbody = cherrypy.request.body.read(int(cl))
body = simplejson.loads(rawbody)
bookFromPost = body.book
# create book and save in data storage
parent_key = ndb.Key('Library', user.user_id())
book = Book(parent=parent_key, isbn=bookFromPost.isbn)
book.put()
You should return a 201 created HTTP code with a json representation of the book with assigned id
book:{id:[the new id],isbn:[the isbn value]}