wiki:DeveloperGuidelines/Tutorial/RESTCustomisation

Version 5 (modified by Dominic König, 11 years ago) ( diff )

--

S3REST Customisation Mini Tutorials

see also: RESTController, S3REST

REST and CRUD

It seems generally difficult for new developers to understand how to take control of functionality in the RESTController of the S3 framework.

IMHO that is because most of the examples look like that:

def myresource():
   """ RESTful CRUD controller """
   return shn_rest_controller(module, resource)

This in fact leaves it all to the shn_rest_controller function - hard to see what really happens.

However, shn_rest_controller does not actually mean you would be tied to the CRUD method handlers of 01_crud.py either. REST merely cares for the proper resolution of URLs and HTTP methods, and then calls hooked-in functions to process the requests.

First of all - shn_rest_controller returns some output:

def myresource():
   """ RESTful CRUD controller """
   output = shn_rest_controller(module, resource)
   return output

In case of interactive views, this output would be a dict with the components that are then passed to the view (by "return output" here). This could contain a form, for example:

def myresource():
   """ RESTful CRUD controller """
   output = shn_rest_controller(module, resource)
   if isinstance(output, dict) and "form" in output:
       form = output.get("form")
       <your form manipulation here>
   return output

That also means, you can add more items to the output dict:

def myresource():
   """ RESTful CRUD controller """
   output = shn_rest_controller(module, resource)
   if isinstance(output, dict):
       if "form" in output:
           form = output.get("form")
           <your form manipulation here>
       myitem = "I can send this item to the view"
       output.update(myitem=myitem)
   return output

Secondly, the shn_rest_controller has multiple hooks, among others for CRUD method handlers. Look at how shn_rest_controller is actually defined:

def shn_rest_controller(module, resource, **attr):

   s3rest.set_handler("import_xml", import_xml)
   s3rest.set_handler("import_json", import_json)
   s3rest.set_handler("list", shn_list)
   s3rest.set_handler("read", shn_read)
   s3rest.set_handler("create", shn_create)
   s3rest.set_handler("update", shn_update)
   s3rest.set_handler("delete", shn_delete)
   s3rest.set_handler("search", shn_search)
   s3rest.set_handler("options", shn_options)

   output = s3rest(session, request, response, module, resource, **attr)
   return output

This is nothing else than a wrapper for the global s3rest object (an instance of S3RESTController), which configures the CRUD handlers of 01_crud.py as handlers for CRUD methods. You don't have to use shn_rest_controller to have a RESTful API, you can make your own handler configuration and call s3rest directly.

But even when using shn_rest_controller, you can take control over what actually happens. A very comfortable (and recommended) way to get control over s3rest when using shn_rest_controller is to hook in prep and postp functions. Look at S3REST to find out when prep and postp hooks are invoked.

A prep hook would allow you to change a handler configuration in certain situations, e.g. testing a URL variable:

def myresource():
   """ RESTful CRUD controller """

   def _prep(jr):
       mylist = jr.request.vars.get("mylist")
       if mylist:
           s3rest.set_handler("list", my_list_controller)
       return True # do not forget to return True!
   response.s3.prep = _prep

   output = shn_rest_controller(module, resource)
   return output

This example would switch to my_list_controller instead of shn_list in case there is a ?mylist= in the URL. In all other cases, the default handlers are executed as usual, you still have a RESTful API for your resources.

While you can define prep's and postp's as local functions (as in the example above) or even lambdas, it is also very well possible to create more generic, reusable prep and postp functions (e.g. to implement different method handler configurations, or to catch certain situations to bypass the CRUD handlers, or to manipulate the output dict in a certain way).

A very important structure during prep and postp is the S3RESTRequest object (usually instantiated as "jr"). This object contains all necessary information to process the current REST request, and it is passed to both prep and postp. See S3REST for a list of attributes and methods.

Custom methods

Let me give you another recipe:

Imagine you have a resource "warehouse" and want to implement a function "report" that generates a report about the current status of the warehouse.

Now you're gonna provide this report in a RESTful way, i.e. you want to provide it as a resource that can be addressed via URLs like:

http://site.myserver.org/eden/wm/warehouse/report

and is provided in several different data formats beyond HTML (the interactive view), let's say - XLS and XML:

http://site.myserver.org/eden/wm/warehouse/report.xls
http://site.myserver.org/eden/wm/warehouse/report.xml

How to tackle this?

Yes - you can use shn_rest_controller for this!

This could be your "warehouse" controller:

def warehouse():
   """ RESTful CRUD controller """
   return shn_rest_controller(module, resource, ...)

At first you implement your report function (in the controller file). This function takes the argument jr (=S3RESTRequest) and a dict of named arguments (just the same named arguments from the shn_rest_controller call above). This function returns the report. Then, in your controller, you plug in this function to your resource - together it would look like that:

def warehouse():
    """ RESTful CRUD controller """
    s3xrc.model.set_method(module, resource, method="report", action=warehouse_report)
    return shn_rest_controller(module, resource, ...)

def warehouse_report(jr, **attr):
    """" Warehouse report """
    <Code to produce the report>
    return report

How to implement the report function now? Well - that's entirely up to you. In case of interactive views, you would usually return a dict of values that are then formatted as HTML in the view template:

def warehouse_report(jr, **attr):
    """" Warehouse report """
    <Code to produce the report>
    # Assemble the report as dict:
    report = dict(title="Page Title", ...)
    return report

Note that if "report" is a dict, then the REST controller automatically adds the S3RESTRequest as "jr" to that dict before returning it. Thus, "jr" is available to the view templates. That also means: do not use "jr" as key in that dict.

However, there may be other formats than HTML - to implement other formats, you might need to check for the representation of the request. This can be found in jr:

def warehouse_report(jr, **attr):
    """" Warehouse report """
    if jr.representation in ("html", "popup"):
        <Code to produce the interactive report>
        # Assemble the report as dict:
        report = dict(title="Page Title", ...)
    elif jr.representation == "xls":
        <Code to produce the XLS report>
    elif jr.representation in shn_xml_export_formats:
        <Code to produce the XML report>
    else:
        session.error = BADFORMAT
        redirect(URL(r=jr.request))
    return report

See S3RESTRequest to find out more about "jr".

To produce the XML report, it is probably sufficient to just export the requested warehouse information in S3XRC-XML, and then use XSLT stylesheets to produce the finally desired XML formats. That's pretty easy:

def warehouse_report(jr, **attr):
    ...
    elif jr.representation in shn_xml_export_formats:
        return export_xml(xrequest)
    ...

Perhaps you want to add an RSS feed:

def warehouse_report(jr, **attr):
    ...
    elif jr.representation == "rss":
        <Code to produce the RSS report>
        return report
    ...

Ah - now I forgot to mention how you can get at the data in the warehouse_report example. Your implementation already supports a variety of URLs:

This URL addresses the report for all warehouses:

http://site.myserver.org/eden/wm/warehouse/report

This URL addresses the report for the warehouse record with ID=1.

http://site.myserver.org/eden/wm/warehouse/1/report

This URL addresses the report in XLS format for the warehouse record with the UUID=123654278 (assuming that you have UUID's in your warehouse table).

http://site.myserver.org/eden/wm/warehouse/report.xls?warehouse.uid=123654278

The S3RESTRequest provides the resource information to your warehouse_report function.

In case a specific record has been requested, you can access it as:

    record = jr.record

If jr.record is None, then the request is targeting all warehouse records, so you'd take:

    table = jr.table
    records = db().select(table.ALL)
    for record in records:
       ...

instead.

NOTE: representation in jr is always all lowercase, there is no differentiation between the ".XML" and ".xml" extension in the URL.

And...not to forget: your warehouse_report is still a controller function, that means you can implement forms as usual (e.g. operated with form.accepts or web2py Crud).

Note: See TracWiki for help on using the wiki.