23. Using generic view groups

Note

This feature is still an experimental. The implementation might change in the future.

23.1. CRUD

23.1.1. CRUD Overview

You can use kay.generics.crud.CRUDViewGroup in order to define generic CRUD views easily. You just need your own model, modelform definition, and your own templates for rendering htmls.

23.1.2. Your first CRUD

Let’s see the simplest example.

myapp/models.py

# -*- coding: utf-8 -*-
# myapp.models

from google.appengine.ext import db

# Create your models here.

class MyModel(db.Model):
  comment = db.StringProperty()

  def __unicode__(self):
    return self.comment

__unicode__ method here is just for rendering entities of this model in a simple way. The default templates for list entities use this method for rendering entities. So, you don’t need this method if you render entities in other way.

myapp/forms.py

from kay.utils.forms.modelform import ModelForm

from myapp.models import MyModel

class MyForm(ModelForm):
  class Meta:
    model = MyModel

This is a very simple modelform.

Then, you can just create an object for CRUD operation in urls.py.

myapp/urls.py

# -*- coding: utf-8 -*-
# myapp.urls

from kay.generics import crud

from myapp.forms import MyForm
from myapp.models import MyModel

class MyCRUDViewGroup(crud.CRUDViewGroup):
  model = MyModel
  form = MyForm

view_groups = [MyCRUDViewGroup()]

That’s all. You may want to add kay.utils.flash.FlashMiddleware for your convenicence.

settings.py

MIDDLEWARE_CLASSES = (
  'kay.utils.flash.FlashMiddleware',
)

Then, you can access ‘/mymodel/list’ for seeing the list of MyModel entities. Here are default mapping rules generated by MyCRUDViewGroup:

Map([[<Rule '/mymodel/list' -> myapp/list_mymodel>,
 <Rule '/mymodel/list/<cursor>' -> myapp/list_mymodel>,
 <Rule '/mymodel/show/<key>' -> myapp/show_mymodel>,
 <Rule '/mymodel/create' -> myapp/create_mymodel>,
 <Rule '/mymodel/update/<key>' -> myapp/update_mymodel>,
 <Rule '/mymodel/delete/<key>' -> myapp/delete_mymodel>]])

You can also use string for the values of model and form class attribute for loading modules lazily as follows:

myapp/urls.py

# -*- coding: utf-8 -*-
# myapp.urls

from kay.generics import crud

class MyCRUDViewGroup(crud.CRUDViewGroup):
  model = 'myapp.models.MyModel'
  form = 'myapp.forms.MyForm'

view_groups = [MyCRUDViewGroup()]

23.1.3. Using your own templates

You can set templates class attribute for using your own templates for rendering html. Here is a simple example:

class MyCRUDViewGroup(crud.CRUDViewGroup):
  model = 'myapp.models.MyModel'
  form = 'myapp.forms.MyForm'
  templates = {
    'show': 'myapp/mymodel_show.html',
    'list': 'myapp/mymodel_list.html',
    'update': 'myapp/mymodel_update.html'
  }

Default templates is set as follows:

templates = {
  'list': '_internal/general_list.html',
  'show': '_internal/general_show.html',
  'update': '_internal/general_update.html',
}

So, for an opener, you can copy kay/_internal/tempaltes/general_***.html to your application’s template directory, and you can edit those files as you like.

23.1.4. Giving additional context on creating/updating entities

Sometimes you need to have some additional values on creating/updating entities other than a modelform takes care about. You can define get_additional_context_on_create or get_additional_context_on_update methods on your own CRUDView classes for this purpose.

These methods must receive request and form instances as arguments, and must return a dictionary. This dictionary will be passed to save() method of your ModelForm instance.

23.1.5. Setting current user as a paticular property

You can use kay.db.OwnerProperty for this purpose. The default value of this property is a current user’s key if user is sienged in, otherwise, None. You need to exclude this property on your form like an example bellow:

myapp/models.py

# -*- coding: utf-8 -*-
# myapp.models

from google.appengine.ext import db
from kay.db import OwnerProperty

# Create your models here.

class MyModel(db.Model):
  user = OwnerProperty()
  comment = db.StringProperty()

  def __unicode__(self):
    return self.comment

myapp/forms.py

from kay.utils.forms.modelform import ModelForm

from myapp.models import MyModel

class MyForm(ModelForm):
  class Meta:
    model = MyModel
    exclude = ('user',)

Then, you can just create an object for CRUD operation in urls.py.

23.1.6. Filter which entity to show on the list

You can control which entity to show on the list by defining a get_query instance method on your own CRUDViewGroup subclass.

An example bellow shows how to show entities owned by current user:

class MyCRUDViewGroup(crud.CRUDViewGroup):
  model = 'myapp.models.MyModel'
  form = 'myapp.forms.MyForm'

  def get_query(self, request):
    return self.model.all().filter('user =', request.user.key()).\
      order('-created')

As you can see, get_query receives only current request object as its argument, and must return Query instance.

23.1.7. Access control

You can limit a particular operation to a particular set of users by defining authorize instance method on your own CRUDViewGroup subclass. These operations are classified in list, show, create, update, delete.

kay.generics package has useful presets for this method, so you can choose one of them if you like.

  • kay.generics.login_required
  • kay.generics.admin_required
  • kay.generics.only_owner_can_write
  • kay.generics.only_owner_can_write_except_for_admin

An example bellow shows how to use one of these presets:

from kay.generics import only_owner_can_write_except_for_admin
from kay.generics import crud

class MyCRUDViewGroup(crud.CRUDViewGroup):
  model = 'myapp.models.MyModel'
  form = 'myapp.forms.MyForm'
  authorize = only_owner_can_write_except_for_admin

TODO: detailed docs about authorize method.

23.2. RESTfull API

23.2.1. RESTfull API overview

You can use kay.generics.rest.RESTViewGroup in order to create RESTfull APIs easily. You can create various handlers for RESTfull services of specified models.

23.2.2. Your first REST

Let’s see a simple example.

myapp/models.py:

# -*- coding: utf-8 -*-
# myapp.models

from google.appengine.ext import db

# Create your models here.

class MyModel(db.Model):
  comment = db.StringProperty()
  created = db.DateTimeProperty(auto_now_add=True)

Its a simple model for just storing comments. You can create RESTfull view groups as follows:

myapp/urls.py:

# -*- coding: utf-8 -*-
# myapp.urls
#

from kay.routing import (
  ViewGroup, Rule
)

from kay.generics.rest import RESTViewGroup

class MyRESTViewGroup(RESTViewGroup):
  models = ['myapp.models.MyModel']

view_groups = [
  MyRESTViewGroup(),
  ViewGroup(
    Rule('/', endpoint='index', view='myapp.views.index'),
  )
]

This will give you following Method/URL combinations for RESTfull access to this model, assuming that myapp is mounted at ‘/’. All the <typeName> in the example bellow is ‘MyModel’ in this case.

  • GET http://yourdomain.example.com/rest/metadata

    • Gets all known types
  • GET http://yourdomain.example.com/rest/metadata/<typeName>

    • Gets the <typeName> type profile (as XML Schema). (If the model is an Expando model, the schema will include an “any” element).
  • GET http://yourdomain.example.com/rest/<typeName>

    • Gets the first page of <typeName> instances (number returned per page is defined by server). The returned list element will contain an “offset” attribute. If it has a value, that is the next offset to use to retrieve more results. If it is empty, there are no more results.
  • GET http://yourdomain.example.com/rest/<typeName>?offset=50

    • Gets the page of <typeName> instances starting at offset 50 (0 based numbering). The offset should generally be filled in from a previous request.
  • GET http://yourdomain.example.com/rest/<typeName>?<queryTerm>[&<queryTerm>]

    • Gets a page of <typeName> instances using a query filter created from the given query terms (with offset features mentioned above). Multiple query terms will be AND’ed together to create the filter. A query filter term has the structure: f<op>_<propertyName>=<value>

      Examples:

      • feq_author=bob@example.com” means include instances where the value of the “author” property is equal to “bob@example.com
      • “flt_count=37&fin_content=value1,value2” means include instances where the value of the “count” property greater than “37” and the value of the content property is “value1” or “value2”

      Available operations:

      • feq_ -> “equal to”
      • flt_ -> “less than”
      • fgt_ -> “greater than”
      • fle_ -> “less than or equal to”
      • fge_ -> “greater than or equal to”
      • fne_ -> “not equal to”
      • fin_ -> “in <commaSeparatedList>”
      • order=param_name will make result set to be ordered

      Blob and Text properties may not be used in a query filter

  • GET http://yourdomain.example.com/rest/<typeName>/<key>

    • Gets the single <typeName> instance with the given <key>
  • POST http://yourdomain.example.com/rest/<typeName>

    • Create new <typeName> instance using the posted data which should adhere to the XML Schema for the type
    • Returns the key of the new instance by default. With “?type=full” at the end of the url, returns the entire updated instance like a GET request.
  • POST http://yourdomain.example.com/rest/<typeName>/<key>

    • Partial update of the existing <typeName> instance with the given <key>. Will only modify fields included in the posted xml data. (Returns same as previous request)
  • PUT http://<service>/rest/<typeName>/<key>

    • Complete replacement of the existing <typeName> instance with the given <key>(Returns same as previous request)
  • DELETE http://<service>/rest/<typeName>/<key>

    • Delete the existing <typeName> instance

By default, you need to create XML elements as the payload for POST and PUT requests, but you can also use json payload by setting “Content-Type” request header to “application/json”.

By default, the result set is served in XML format, but you can also get json response by setting “Accept” request header to “application/json” as well.

23.2.3. Ajax example

Here is an example for guestbook implementation with using jquery’s ajax request.

myapp/templates/index.html:

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Top Page - myapp</title>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4/jquery.min.js"></script>
<script type="text/javascript">
function deleteEntity(key) {
  $.ajax({
    type: "DELETE",
    url: "/rest/MyModel/"+key,
    success: function(data) {
      refreshData();
    }
  });
}
function displayEntity(entity) {
  $("#comments").append(entity.comment+
    "<i> at " + entity.created + "</i>"+
    '&nbsp;<a href="#" onclick="deleteEntity(\''+entity.key+'\');">x</a><br>');
}
function refreshData() {
  $.ajax({
    type: "GET",
    url: "/rest/MyModel?ordering=-created",
    dataType: "json",
    success: function(data) {
      $("#comments").html("");
      if (data.list.MyModel) {
        if (data.list.MyModel.key) {
          displayEntity(data.list.MyModel);
        } else {
          for (var i=0; i < data.list.MyModel.length; i++) {
            displayEntity(data.list.MyModel[i]);
          }
        }
      }
    }
  });
  $("#comment").focus();
}
function sendData() {
  $("#sendButton").attr("disabled", "disabled");
  $.ajax({
    type: "POST",
    url: "/rest/MyModel?type=full",
    dataType: "json",
    contentType: "application/json",
    data: JSON.stringify({"MyModel": {"comment": $("#comment").val()}}),
    success: function(data) {
      $("#comment").val("");
      $("#sendButton").attr("disabled", "");
      refreshData();
    }
  });
}
$(document).ready(function(){
  $("#comment").keypress(function(e) {
    if (e.which == 13) {
      sendData();
    }
  });
  refreshData();
});
</script>
</head>
<body>
<input type="text" id="comment">
<input type="button" onclick="sendData();" value="send" id="sendButton">
<div id="comments"></div>
</body>
</html>