Version 64 (modified by 13 years ago) ( diff ) | ,
---|
Table of Contents
S3XRC | S3 RESTful API | s3_rest_controller
s3_rest_controller
Introduction
The so-called REST Controller (function s3_rest_controller()
) is a helper function to easily apply the RESTful API of the S3Resource class to your controller.
s3_rest_controller
does:
- parse and execute the incoming HTTP request on the specified resource
- populate and hand-over view variables
- choose and set the response view template (
response.view
)
Using s3_rest_controller
, a basic RESTful controller for the pr_image
table can look like:
def image(): """ RESTful CRUD controller """ return s3_rest_controller("pr", "image")
This exposes all standard URLs and methods for this table, including:
- interactive create, read, update, delete and list views
- non-interactive data export/import (GET/POST/PUT/DELETE) in multiple formats
Basic Syntax
output = s3_rest_controller(prefix, resourcename)
- prefix is the application prefix of the resource
- resourcename is the name of the resource (without prefix)
- output contains the result of the request and can be returned from the controller as-is
- in interactive view formats, this is a
dict
of view variables
- in interactive view formats, this is a
Basic Options
All of the following options are set using:
s3xrc.model.configure(table, key=value)
where:
- table is the respective DB table
- key is the setting key
- value is the configuration value
You can specify multiple settings at once:
s3xrc.model.configure(table, key1=value1, key2=value2, ...)
configure overrides any settings that have been made for that table before (e.g. in the model), where only the specified keys are changed while all others are left untouched.
You can also delete a particular setting by:
s3xrc.model.clear_config(table, "key")
where "key" must be the respective key as string.
CRUD Options
You can define which of the CRUD functions are allowed for your resource. By default, they all are True.
s3xrc.model.configure(table, editable=True, # Records can be updated insertable=True, # New records can be added deletable=True, # Records can be deleted listadd=True) # to have a add-record form in list view
Note:
- these settings are completely independent from the user's permission to perform the respective actions. If you have set
insertable=False
, then adding records to this table will be impossible even if the user has the permission to add records. - in standard views, the add-form in list views is hidden and can be activated by clicking the respective "Add Record" button. If
listadd=False
, then the "Add Record" button will redirect to the create-form view instead. - these settings do not effect non-interactive representations
List-Fields
By default, all readable fields of a table appear in list views.
To control which fields are to be included in list views, to include virtual fields or fields in referenced tables, or to change the order of the columns, use the list_fields
CRUD setting.
This setting takes a list of field names of the fields to be displayed:
s3xrc.model.configure(table, list_fields=["id", "name", "location_id"])
NOTE: If your view uses dataTables (which all default view templates do), then you must include
"id"
at the first place to have it working properly.
For virtual fields, you should provide a tuple of (T("FieldLabel"), "fieldname")
, because virtual fields do not have a .label
setting (as they are functions and not Field
instances):
s3xrc.model.configure(table, list_fields=[ "id", "name", "location_id", (T("Total costs"), "total_costs") ]
You can use the tuple notation for any other field as well, in order to override the field label for this list view.
To include a field from a referenced table, insert the field name as "<foreign_key>$<fieldname>", e.g.:
s3xrc.model.configure(table, list_fields=[ "id", "name", "location_id$lat", "location_id$lon" ]
List-add
By default, list views will contain a hidden create-form and an "Add Record"-button to activate the create-form (provided that the user is authorized to add records to this resource).
If the resource is set to insertable=False
or if the user is not permitted to create records in this table, no "Add Record"-button will be available.
NOTE: If for some reason the default embedded create-form is not desired, you can set
s3xrc.model.configure(table, listadd=False, addbtn=True)
, which will render the "Add Record"-button as link to the create method instead.
Redirection
Default destination of the redirection after a create or update is the read view of the processed record, after a delete it is the list view of the respective table.
The redirection destination can be configured per DB table, using:
s3xrc.model.configure(table, create_next=url)
s3xrc.model.configure(table, update_next=url)
s3xrc.model.configure(table, delete_next=url)
where:
- table is the respective DB table
- url is the URL to redirect to
If, for create_next or update_next, url contains the string "[id]" (or its URL-encoded equivalent), then this string will be replaced by the ID of the updated/newly created record before redirection.
Note:
- redirection does not happen after non-interactive data imports!
Callbacks
For every DB table, you can define functions to be invoked upon certain CRUD events. Those "callbacks" can be:
- a single callable (function, lambda, callable object)
- a list of callables, which are invoked in list order
- a dict of callables, where the tablename is used as key to find the callable to be invoked
- a dict of lists of callables, where the tablename is used as key to find the list of callables to be executed in list order
The return value of the callables, if any, is ignored.
Important:
Callbacks are invoked in the same manner during non-interactive data imports, where usually multiple records will be processed in one and the same request. Therefore, any callbacks must not redirect!
Validation Callbacks
You can define extra form validation methods to be invoked after a create/update form has successfully passed the indivdual field validation, by using:
s3xrc.model.configure(table, create_onvalidation=callback)
s3xrc.model.configure(table, update_onvalidation=callback)
where:
- table is the respective DB table
- callable is the callback setting, see Callbacks
If either of create_onvalidation
or update_onvalidation
is not set, then the onvalidation
setting is tried:
s3xrc.model.configure(table, onvalidation=callback)
This allows you to define a common onvalidation callback for both create and update.
Onvalidation callbacks are meant to allow additional form data validation (beyond individual field validators). The callback functions receive the form as first and only parameter, while their return values will be ignored.
Any validation errors are to be reported directly into the form as:
form.errors[fieldname] = error_msg
where:
- fieldname is the field containing the invalid value
- error_msg is the error message to be displayed in the form close to that field
If after the execution of the onvalidation callback any messages are found in form.errors
, then no data are being imported and instead, the process will return to the input view with the messages displayed close to the respective form fields.
In non-interactive data imports, the error message will be added to the import tree as extra attribute of the invalid element. The XML importer will however process all records in the import tree in order to find all validation errors before reporting the invalid tree to the sender, and in case ignore_errors
is used, all valid records will be imported in the first attempt.
On-accept Callbacks
You can define methods to be invoked after a record has been created/updated, by using:
s3xrc.model.configure(table, create_onaccept=callback)
s3xrc.model.configure(table, update_onaccept=callback)
where:
- table is the respective DB table
- callable is the callback setting, see Callbacks
If either of create_onaccept
or update_onaccept
is not set, then the onaccept
setting is tried:
s3xrc.model.configure(table, onaccept=callback)
This allows you to define a common onaccept callback for both create and update.
The onaccept
callbacks are meant to perform extra post-processing of the newly created/updated record (e.g. to update dependent records). The callback functions receive the respective record as first and only parameter, while their return value will be ignored.
On-Delete Callback
You can define methods to be invoked after a record has been deleted, by using:
s3xrc.model.configure(table, ondelete=callback)
where:
- table is the respective DB table
- callable is the callback setting, see Callbacks
The ondelete
callbacks are meant to perform extra post-processing of the deleted record (e.g. to update dependent records). The callback functions receive the respective record as first and only parameter, while their return value will be ignored.
Note:
At the time when the callback is invoked, the record is already deleted from the database.
Pagination
The default pagination method is server-side (SSPag), meaning, in list views the client will receive only the first of the available rows, and then retrieve more rows as needed by subsequent Ajax calls.
In contrast to that, in client-side pagination (CSPag) mode all available rows of the list are retrieved and send to the client at once. For most tables, though, this will probably be a huge data set and take a long time to extract and transmit, while mostly being unnecessary when the user only needs to see the first 20 rows to find what he's looking for.
However, some tables may by their nature only contain one or few rows, and then server-side pagination is not needed (in fact, inefficient). In these cases, the respective controller can turn it off by:
response.s3.no_sspag=True
View Control
In web2py, the default view is chosen after the name of the controller, i.e. if the controller is person(), then the default view is person.html.
s3_rest_controller() modifies this schema in order to allow you to create method-specific views for the same controller.
Default View
The default view is chosen in a fallback cascade, which is:
- <prefix>/<resource-name>_<component-name>_<method>.html
- <prefix>/<resource-name>_<method>.html
- <method>.html
where:
- prefix is the application prefix of the resource name (e.g. "pr")
- resource-name is the name of the resource (e.g. "person")
- component-name is the name of the component resource (e.g. "address", only if the request targets a component resource)
- method is one of:
- display
- list
- list_create (to be deprecated)
- create
- update
- delete
- search
Example:
http://localhost:8000/eden/pr/person/create.html
is looking for one of pr/person_create.html, pr/create.html or finally create.html as default view.
If none of these is found, "default.html" serves as catch-all fallback.
Custom View
To choose a custom view, you can easily override the default setting after s3_rest_controller returns:
output = s3_rest_controller(prefix, resourcename) response.view = "myview.html" return output
Additional View Variables
In interactive view formats, any additional named arguments in the s3_rest_controller
argument list will be added to the view variables:
output = s3_rest_controller(prefix, resourcename, **attr)
- attr: additional view variables
- any callable argument will be invoked with the
S3Request
as first and only argument, and its return value will be added to the view variables - any non-callable argument will be added to the view variables as-is
- any argument that gives
None
will remove this key from the view variables
A typical use-case is rheader:
def my_rheader(r): if r.interactive and r.component: # Code producing the rheader... return rheader else: return None output = s3_rest_controller(prefix, name, rheader=my_rheader)
If my_rheader(r)
gives something else than None
, then this value is added as rheader
to the view variables.
Advanced Options
Filtering Lists
You can filter lists in the controller by setting response.s3.filter
to a filter query:
# This filters for females: response.s3.filter = (db.pr_person.gender == 2) return s3_rest_controller("pr", "person")
Note that this only takes effect in the main controller (not in prep or postp).
Note that response.s3.filter
affects both, the primary resource and components!
In prep
, you can also add filter queries using the add_filter
method:
def prep(r): resource = r.resource query = (db.pr_address.type == 1) # Home addresses only resource.add_filter(query) return True response.s3.prep = prep return s3_rest_controller("pr", "person")
However, add_filter
again affects both, primary and component records - so this example would:
- only retrieve
person
records which have a type 1address
record - only retrieve the
address
records with type 1.
This can be an unwanted side-effect.
To have the primary resource unfiltered, and filter only records in a particular component, you can use add_component_filter
:
def prep(r): resource = r.resource query = (db.pr_address.type == 1) # Home addresses only resource.add_component_filter("address", query) return True response.s3.prep = prep return s3_rest_controller("pr", "person")
In this case, all person
records would be selected - while only address
records of type 1 would be retrieved.
Pre-populating Create-Forms
Create-forms can be pre-populated with data by one of these 3 methods:
- model defaults (standard)
- values from another record in the database
- values provided by the controller
Model defaults are defined per field and can be set as db.my_table.field.default = value
at any time before the REST method is applied (even in the pre-process).
Values from another record in the database can be used via a URL query like:
- /my/resource/create?from_record=id&from_fields=fieldname1,fieldname2,...
The id of the original record can also be specified as tablename.id, if the original record is in another table. Additionally, fieldnames can be specified as fieldname$original_fieldname to map between different fieldnames.
To pre-populate Create-forms from the controller, you can specify the variable populate
in the arguments of s3_rest_controller
:
output = s3_rest_controller(prefix, resourcename, populate=dict(fieldname1=value1, fieldname2=value2)) return output
Instead of a dict
, you can also pass a callable object as populate
. This will be executed with the current S3Request
and the named arguments of s3_rest_controller
in order to produce the field/value dict
:
def populate(r, **attr): """ Helper function to pre-populate create-forms """ # some code to produce the data # ... # return the dict return dict(fieldname1=value1, fieldname2=value2) output = s3_rest_controller(prefix, resourcename, populate=populate) return output
Note that populate
will only be applied in GET
requests and only if no record_id is specified. That means, if it uses a separate form to generate the data, you need to revert the request into GET
in order to have the create-form pre-populated:
data = None form = FORM(...some form...) output = dict(helper_form=form) if form.accepts(request.vars, session, formname="helper_form"): output = dict() # remove the helper form data = Storage(...) # some code to extract the data from the helper form if data: request.env.request_method = "GET" # revert to GET if data available _output = s3_rest_controller(prefix, resourcename, populate=data) if isinstance(_output, dict): output.update(_output) else: output = _output return output
NB: This construction could be used e.g. to loop in an OCR client into a REST controller. The helper form would then be a file upload form which is displayed alongside with the normal create-form - so the user can either enter data manually and submit the create-form, or first submit a file to pre-populate the create form, and then edit the data and submit the create-form.
Pre-Process
- coming soon...
Passing information between main controller & pre-processor
Scope normally means that these 2 sections can only talk to each other via globals or the Request object.
If you need to pass data between them, you can use this trick:
vars = {} # the surrounding dict def prep(r, vars): vars.update(x=y) # the actual variable to pass is x return True response.s3.prep = lambda r, vars=vars: prep(r, vars) output = shn_rest_controller(module, resource) x = vars.get(x, None)
An example usage is in controllers/gis.py
for location()