|Version 10 (modified by 13 years ago) ( diff ),|
S3REST Customisation Mini Tutorials
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", None) if form: # Your code to manipulate the form goes 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): form = output.get("form", None) if form: # Code to manipulate form goes 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 """ # Define pre-processor as local function: 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! # Hook pre-processor into REST controller: 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.
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:
and is provided in several different data formats beyond HTML (the interactive view), let's say - XLS and XML:
How to tackle this? Yes - you can use shn_rest_controller for this!
This could be your "warehouse" CRUD 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+Reports controller """ # Plug warehouse_report into warehouse resource: s3xrc.model.set_method(module, resource, method="report", action=warehouse_report) return shn_rest_controller(module, resource, ...) def warehouse_report(jr, **attr): """ Warehouse report generator """ # Code to produce the report goes here 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 generator """ # Code to produce the report items: title = T("Warehouse Report") # Assemble the report items in a dict: report = dict(title=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 generator """ if jr.representation in ("html", "popup"): # Code to produce the report items: title = T("Warehouse Report") # Assemble the report items in a dict: report = dict(title=title, ...) elif jr.representation == "xls": # Code to produce the XLS report goes here ... elif jr.representation in shn_xml_export_formats: # Code to produce the XML report goes here ... 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: report = 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 goes here 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:
This URL addresses the report for the warehouse record with ID=1.
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).
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: ...
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).