On this post I continue to port Udacity course CS253 Web Development from python to Go. This time I am working on Unit 3. This is a long unit with a lot of ground so let's get started.
You can check out my previous blog post if you haven't done it yet:
The main architecture of the NewPostHandler is the following:
A GET
method to display the Form.
A POST
method to read the html Form, create the post and do a redirection to the created post.
// NewPostHandler is the HTTP handler to create a new Post
func NewPostHandler(w http.ResponseWriter, r *http.Request){
c := appengine.NewContext(r)
if r.Method == "GET" {
// display the form
}else if r.Method == "POST"{
// read the html form
// create a post entity
// redirect to the created post
}
}
The NewPostHandler displays a simple Form to create a new post. This is how it looks like:
<form method="post">This template goes hand in hand with this type structure to hold the new post information:
<label>
<div>subject</div>
<input type="text" name="subject" value="{{.Subject}}">
</label>
<label>
<div>content</div><textarea name="content">{{.Content}}</textarea>
</label>
<div class="error">{{.Error}}</div>
<br>
<input type="submit">
</form>
// NewPostForm is the type used to hold the new post information.We display the Form by calling writeNewPostForm:
type NewPostForm struct{
Subject string
Content string
Error string
}
// executes the newpost.html template with NewPostForm type as param.
func writeNewPostForm(c appengine.Context, w http.ResponseWriter, postForm *NewPostForm){
tmpl, err := template.ParseFiles("templates/newpost.html")
if err != nil{
c.Errorf(err.Error())
}
err = tmpl.Execute(w,postForm)
if err != nil{
c.Errorf(err.Error())
}
}
As always we read the information from the form by calling r.FormValue()
, then as in the previous unit we check the validity of the inputs before doing any further work.
postForm := NewPostForm{Once we are sure about the inputs, we can create a blog post. The first step, as for the python version, is to have an entity Post:
r.FormValue("subject"),
r.FormValue("content"),
"",
}
if !(tools.IsStringValid(postForm.Subject) &&
tools.IsStringValid(postForm.Content)){
postForm.Error = "We need to set both a subject and some content"
writeNewPostForm(w, &postForm)
}else{
// create a blog post here.
// ...
}
// Post is the type used to hold the Post information.Note: You will need to import
type Post struct {
Id int64
Subject string
Content string
Created time.Time
}
"appengine/datastore"
to perform the following operations.postID, _, _ := datastore.AllocateIDs(c, "Post", nil, 1)I decided to store the postID into the entity, it is easiear the retreive it later in order to perform a permalink redirect
key := datastore.NewKey(c, "Post", "", postID, nil)
p := models.Post{
postID,
postForm.Subject,
postForm.Content,
time.Now(),
}
key, err := datastore.Put(c, key, &p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
"/blog/postid"
. In python this is easier as you can just do the following: p.key().id()
def post(self):where blog_key returns a Key for the blog entity:
user_subject = self.request.get('subject')
user_content = self.request.get('content')
subject = valid_str(user_subject)
content = valid_str(user_content)
if not(subject and content):
self.write_form("We need to set both a subject and some content",user_subject,user_content)
else:
p = Post(parent = blog_key(), subject = user_subject,content = user_content)
p.put()
#redirect to permalink
self.redirect("/unit3/blog/%s" % str(p.key().id()))
def blog_key(name = 'default'):Next step now, perform a redirection to the postID permalink:
return db.Key.from_path('blogs', name)
// build url and redirectthe Post entity is in the datastore now. We can now deal with the Permalink handler.
permalinkURL := "/blog/"+strconv.FormatInt(p.Id,10)
http.Redirect(w, r, permalinkURL, http.StatusFound)
The first thing to work on when starting the permalink is how to dispatch the handlers.
In python this is easily done as the WSGIApplication is able to parse regular expressions.
('/unit3/blog/?',BlogFront),In Go you cannot do this so I had to do some searching and this is the solution I came up with.
('/unit3/blog/newpost',NewPostHandler),
('/unit3/blog/([0-9]+)', Permalink),
init()
method looks like after using the RegexpHandler
:func init(){And this is how the RegexpHandler looks like:
h := new( tools.RegexpHandler)
h.HandleFunc("/",mainHandler)
h.HandleFunc("/blog/?", unit3.BlogFrontHandler)
h.HandleFunc("/blog/newpost", unit3.NewPostHandler)
h.HandleFunc("/blog/[0-9]+/?",unit3.PermalinkHandler)
http.Handle("/",h)
}
package toolsNow that my permalinkHandler is ready we can focus on the permalink handler implementation.
import (
"net/http"
"regexp"
)
type route struct {
pattern *regexp.Regexp
handler http.Handler
}
type RegexpHandler struct {
routes []*route
}
func (h *RegexpHandler) Handler(pattern *regexp.Regexp, handler http.Handler) {
h.routes = append(h.routes, &route{pattern, handler})
}
func (h *RegexpHandler) HandleFunc(strPattern string, handler func(http.ResponseWriter, *http.Request)) {
// encapsulate string pattern with start and end constraints
// so that HandleFunc would work as for Python GAE
pattern := regexp.MustCompile("^"+strPattern+"$")
h.routes = append(h.routes, &route{pattern, http.HandlerFunc(handler)})
}
func (h *RegexpHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
for _, route := range h.routes {
if route.pattern.MatchString(r.URL.Path) {
route.handler.ServeHTTP(w, r)
return
}
}
// no pattern matched; send 404 response
http.NotFound(w, r)
}
First thing to do is retrieve the id from the url. In python this is really easy as the id is a parameter of your get()
method:
class Permalink(renderHandler):In Go I did not find a way to do this so I had to retreive the id from the URL.
def get(self, post_id):
# do something awesome with post_id
func PermalinkHandler(w http.ResponseWriter, r *http.Request){Now that I have the post ID I can perform a query to the datastore and cache it into memcache if it does not yet exist.
if r.Method == "GET" {
path := strings.Split(r.URL.String(), "/")
intID, _ := strconv.ParseInt(path[2], 0, 64)
// do something awesome with the intID
I created a function called PostAndTimeByID which returns me the following structure:
// PostAndTime is the type used to hold an entity Post and it's "cache hit time" information.I had to work a little bit more on this as you need to encode the information you want to put in the cache. To add the PostAndTime structure to memcache you have to encapsulate first in a memcache Item.
type PostAndTime struct{
Post Post
Cache_hit_time time.Time
}
func Add(c appengine.Context, item *Item) errorAn Item has a Key and its value. The value needs to be a slice of bytes. So to pass the bytes of the PostAndTime structure, I decided to use gob (the other option was to use the json encoder) to encode and decode the bytes of my structure.
Here is how to encode the structure to bytes with gob and set it to memcache:
// record information in cache for next timeNote: You need to register the type you want to encode before doing the above code, else it will panic. This is done by calling the
mCache := new(bytes.Buffer)
encCache := gob.NewEncoder(mCache)
encCache.Encode(postAndTime)
postItem := &memcache.Item{
Key: memcacheKey,
Value: mCache.Bytes(),
}
if err := memcache.Add(c, postItem); err == memcache.ErrNotStored {
c.Errorf("cs253: postAndTime with key %q already exists", item.Key)
} else if err != nil {
c.Errorf("error adding item: %v", err)
}
Register
method. It is important to mention that you cannot register a type more than once or it will panic as well so the best is to have this in the init function of your package as follows:func init(){When the item is found in the memcache, you will have to do the opposite and decode the bytes into the structure. This is how I did it:
gob.Register(PostAndTime{})
}
//Memcache item found
var postAndTime PostAndTime
pCache := bytes.NewBuffer(item.Value)
decCache := gob.NewDecoder(pCache)
decCache.Decode(&postAndTime)
Here is the full code for PostAndTimeByID function:
// PostAndTimeByID returns a PostAndTime for the requested idNote some memcache operations like the Get method:
func PostAndTimeByID(c appengine.Context, id int64)( PostAndTime){
memcacheKey := "posts_and_time"+strconv.FormatInt(id, 10)
var postAndTime PostAndTime
//query cache first with memcache key
if item, err := memcache.Get(c, memcacheKey); err == memcache.ErrCacheMiss {
//item not in the cache : will perform query instead
key := datastore.NewKey(c, "Post", "", id, nil)
if err := datastore.Get(c, key, &postAndTime.Post); err != nil {
c.Errorf("cs253: post not found : %v", err)
}
// get current hit time
postAndTime.Cache_hit_time = time.Now()
// record information in cache for next time
mCache := new(bytes.Buffer)
encCache := gob.NewEncoder(mCache)
encCache.Encode(postAndTime)
postItem := &memcache.Item{
Key: memcacheKey,
Value: mCache.Bytes(),
}
if err := memcache.Add(c, postItem); err == memcache.ErrNotStored {
c.Errorf("cs253: postAndTime with key %q already exists", item.Key)
} else if err != nil {
c.Errorf("error adding item: %v", err)
}
} else if err != nil {
c.Errorf("cs253: Memcache error getting item: %v",err)
} else {
//Memcache item found
pCache := bytes.NewBuffer(item.Value)
decCache := gob.NewDecoder(pCache)
decCache.Decode(&postAndTime)
}
return postAndTime
}
memcache.Get(c, memcacheKey)and Add method:
memcache.Add(c, postItem)Also notice the datastore operation:
datastore.Get(c, key, &postAndTime.Post)
Once you have the post and the memcache hit time you can render the permalink page. This time we are defining the template as a separate file and we are using two templates to do this, a permalink.html template for the structure of the page and a post.html template of the post information (the post.html will be used later on in another handler).
This is how the permalink.html template looks like:
{{define "permalink"}}and the post.html template looks like this:
<!DOCTYPE html>
<html>
<head>
<link type="text/css" rel="stylesheet" href="/static/main.css" />
<title>CS 253 Blog in Go!</title>
</head>
<body>
<a href="/blog" class="main-title">
Blog
</a>
{{template "post" .Post}}
<div class="age">
queried {{.Cache_hit_time}} seconds
</div>
</body>
</html>
{{end}}
{{define "post"}}Notice that I pass
<div class="post">
<div class="post-heading">
<div class="post-title">
<a href="/blog/{{.Id}}" class="post-link">{{.Subject}}</a>
</div>
<div class="post-date">
{{.Created.Format "02-01-2006 15:04:05"}}
</div>
</div>
<div class="post-content">
{{.Content}}
</div>
</div>
{{end}}
.Post
to the post template like this:
{{template "post" .Post}}To render these two templates together we have to parse them first and then execute the permalink template.
// writePermalink executes the permalink.html template with a PostAndTime type as param.This is all concerning the permalink handler, next is the blog front handler where we display the first 10 blog posts.
func writePermalink(c appengine.Context, w http.ResponseWriter, p models.PostAndTime){
tmpl, err := template.ParseFiles("templates/permalink.html","templates/post.html")
if err != nil{
c.Errorf(err.Error())
}
err = tmpl.ExecuteTemplate(w,"permalink",p)
if err !=nil{
c.Errorf(err.Error())
}
}
The BlogFrontHandler is a simple one. It performs a query for the recent posts and renders them. This is how it looks:
// BlogFrontHandler is the HTTP handler for displaying the most recent posts.The RecentPosts function performs a query in the datastore as follows:
func BlogFrontHandler(w http.ResponseWriter, r *http.Request){
c := appengine.NewContext(r)
if r.Method == "GET" {
posts := models.RecentPosts(c)
writeBlog(w, posts)
}
}
// RecentPosts returns a pointer to a slice of Posts.The writeBlog function uses two templates, the post.html template and the blog.html template.
// orderBy creation time, limit = 20
func RecentPosts(c appengine.Context)([]*Post){
q := datastore.NewQuery("Post").Limit(20).Order("-Created")
var posts []*Post
if _, err := q.GetAll(c, &posts); err != nil {
c.Errorf("cs253: Error: %v",err)
return nil
}
return posts
}
{{define "blog"}}writeBlog looks exactly as writePost (I should factorize this at some point...)
<!-- some html content -->
<div id="content">
{{range $i, $p :=.}}
{{template "post" $p}}
{{end}}
<div class="age">
queried cache_last_hit seconds ago
</div>
</div>
<!-- some closing tags -->
{{end}}
// writeBlog executes the blog template with a slice of Posts.This covers the third CS253 unit. Next time will be about Unit 4.
func writeBlog(c appengine.Context, w http.ResponseWriter, posts []*models.Post){
tmpl, err := template.ParseFiles("templates/blog.html","templates/post.html")
if err != nil{
c.Errorf(err.Error())
}
err = tmpl.ExecuteTemplate(w,"blog",posts)
if err != nil{
c.Errorf(err.Error())
}
}