How to use AJAX with Google App Engine (Python)
Asked Answered
E

2

11

I am completely novice at AJAX. I am familiar with HTML/CSS, jQuery and beginner at GAE and Python.

In an effort to understand how AJAX works, I would like to know how AJAX might be used (actual code) in this example below. Let's use a reddit-like example where vote ups/downs are ajaxified:

Here is the Story Kind:

class Story(ndb.Model):
    title = ndb.StringProperty(required = True)
    vote_count = ndb.IntegerProperty(default = 0)

The HTML would look like this:

<h2>{{story.title}}</h2>
<div>
    {{story.vote_count}} | <a href="#">Vote Up Story</a>
</div>

How does AJAX fit inside here?

Enoch answered 26/2, 2014 at 20:1 Comment(0)
W
26

Ok Sir here we go... A simple app with one story and infinite votes... ;-)

app.yaml

application: anotherappname
version: 1
runtime: python27
api_version: 1
threadsafe: true

default_expiration: "0d 0h 5m"

libraries:
- name: jinja2
  version: latest

- name: webapp2
  version: latest

handlers:
- url: .*
  script: main.app

main.py

import logging
from controllers import server
from config import config
import webapp2


app = webapp2.WSGIApplication([
        # Essential handlers
        ('/', server.RootPage),
        ('/vote/', server.VoteHandler)
    ],debug=True, config=config.config)


# Extra Hanlder like 404 500 etc
def handle_404(request, response, exception):
    logging.exception(exception)
    response.write('Oops! Naughty Mr. Jiggles (This is a 404)')
    response.set_status(404)

app.error_handlers[404] = handle_404

models/story.py

from google.appengine.ext import ndb


class Story(ndb.Model):
    title = ndb.StringProperty(required=True)
    vote_count = ndb.IntegerProperty(default = 0)

controllers/server.py

import os
import re
import logging
import config
import json

import webapp2
import jinja2

from google.appengine.ext import ndb
from models.story import Story


class RootPage(webapp2.RequestHandler):
    def get(self):
        story = Story.get_or_insert('Some id or so', title='A voting story again...')
        jinja_environment = self.jinja_environment
        template = jinja_environment.get_template("/index.html")
        self.response.out.write(template.render({'story': story}))


    @property
    def jinja_environment(self):
        jinja_environment = jinja2.Environment(
            loader=jinja2.FileSystemLoader(
                os.path.join(os.path.dirname(__file__),
                             '../views'
                ))
        )
        return jinja_environment


class VoteHandler(webapp2.RequestHandler):
    def post(self):
        logging.info(self.request.body)
        data = json.loads(self.request.body)
        story = ndb.Key(Story, data['storyKey']).get()
        story.vote_count += 1
        story.put()
        self.response.out.write(json.dumps(({'story': story.to_dict()})))

and finally

views/index.html

<!DOCTYPE html>
<html>
    <head>
        <base href="/">
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.0/jquery.min.js"></script>
    </head>
    <body>
        <h2>{{story.title}}</h2>
        <div>
            <span class="voteCount">{{story.vote_count}}</span>  | <a href="javascript:VoteUp('{{story.key.id()}}');" >Vote Up Story</a>
        </div>
        <script>
            function VoteUp(storyKey){
                $.ajax({
                  type: "POST",
                  url: "/vote/",
                  dataType: 'json',
                  data: JSON.stringify({ "storyKey": storyKey})
                })
                .done(function( data ) { // check why I use done
                    alert( "Vote Cast!!! Count is : " + data['story']['vote_count'] );
                    $('.voteCount').text(data['story']['vote_count']);
                });
            };
        </script>
    </body>
</html>

Assemble, read it's simple enough and run. If you need a working git example just comment.

githublink (as from comments)

Whitelaw answered 26/2, 2014 at 21:36 Comment(8)
Wow! This is amazing effort dedicated in your answer, @JimmyKane. I'll need some time to break this down. If it's no trouble, I wouldn't mind the working git version. Thank you so much.Enoch
@haopei Sorry I forgot you. Here you go github.com/jimmykane/gae-voting-ajax-example . I hope you star it ;-)Whitelaw
can you briefly explain what self.request.body does? I ran a search on the webapp and appengine documentations and found very little on it. It almost seems there is no definition for it in the docs. Thank you.Enoch
@haopei webapp-improved.appspot.com/guide/request.html check this. It's documented there... If you need more help mention.Whitelaw
I do have one issue. I followed the steps above, and I run into a problem during this line self.response.out.write(json.dumps(({'story': story.to_dict()}))). My Story model includes a ndb.DateTimeProperty, and I am getting the error 'datetime.datetime' object has no attribute 'to_dict'. Any idea how to get pass this?Enoch
@haopei Datetime properties are not json serializable. Try excluding the property like so: story.to_dict(exclude='my_datetime') and then convert and attach it to the dict. Check here for the to_dict and how to use it developers.google.com/appengine/docs/python/ndb/… . Also datetimes are a pain. If you need further assistance please mention here again and I ll try to add the datetime serialization techniWhitelaw
Thank you so much again, @JimmyKane. This is a monumental amount of help. I should note that the exclude parameter takes a list, tuple or set, so actually this works instead: story.to_dict(exclude=["my_datetime"]). I wouldn't mind learning about datetime serialization when I need it. I may have to call on you again in the future :) Appreciate your help!Enoch
@haopei correct noting about list/tuple. :-) thanks for the good words. Made psycho boost this morning.Whitelaw
C
0

Here is a little prototype web app on GitHub to test how to handle error messages in HTML form submissions with AJAX, Python and Google App Engine. It will give a starting point to see how these three pieces of technology mesh together. You can test this "app" on https://ajax-prototype.appspot.com/

Here is how it works on the client-side:

  • This htlm form submission is used:

    <form method="post" action="javascript:ajaxScript();">
    <label>Please pick a name</label>
    <input id="input" type="text">
    <input type="submit">
    <div id="error" style="color:red"></div>
    

  • It will trigger the JavaScript function ajaxScript:

    function ajaxScript() {
        var input = $("#input").val();
        $.ajax({
            type: "POST",
            url: "/",
            data: JSON.stringify({
                "name": input
            }),
            dataType: "json"
        })
            .done(function(jsonResponse) {
                $("#error").html(jsonResponse.message);
            });
    }
    
  • The jQuery .ajax() method handles the request while the .done() method will eventually handle the response that it gets from the server.

On the server-side:

  • The main.py file handles the server side of the business using our handler class AjaxHandler, which inherits from the GAE builtin class webapp2.RequestHandler

  • Within this class, the post method handles the AJAX request:

    def post(self):
        data = json.loads(self.request.body)
        username = data["name"]
    
        if not re.match(r"^[a-zA-Z0-9_-]{3,20}$", username):
            if len(username) < 3:
                message = "Your name must be at least 3 characters long."
            else:
                message = "Allowed characters are \
                           a-z, A-Z, 0-9, underscores \
                           and hyphens."
        else:
            message = "Congrats!"
    
        self.response.write(json.dumps({"message": message}))
    
  • Basically, the handy json module provides the two key Python ingredients

    • json.loads handles the data that the browser sends to the server.
    • json.dumps handles the data sent by the server in response to the browser's request.
    • The self.request.body argument of json.loads is the only less common piece of GAE that is used in the process, as it is specific to the task. As its name suggests, it gets the body from the Ajax request sent by the client.
Crimp answered 25/11, 2016 at 13:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.