wiki:RESTController

Version 77 (modified by Pat Tressel, 11 years ago) ( diff )

--

REST Controller

Introduction

The so called REST Controller (function s3_rest_controller()) is a wrapper function for the RESTful API of the S3Resource class.

The S3Resource class implements a generic RESTful API (Representational State Transfer) for Sahana Eden database resources, which maps HTTP requests (i.e., URLs and HTTP methods like GET, PUT, DELETE etc.) to internal data stores (e.g. database resources) and methods (e.g. CRUD), and applies the methods by calling so called method handlers.

Method handlers are to be provided by the calling controller via hooks. The function s3_rest_controller() simplifies the use of the REST interface and provides a number of pre-configured standard CRUD method handlers.

These default method handlers as well as s3_rest_controller() itself are implemented in:

  • modules/s3/s3crud.py.

The method handlers of s3crud.py support a number of data representation formats:

  • HTML (including autogenerated Create/Update forms)
  • PDF (export only)
  • XLS (export only)
  • CSV

Note: Method handlers for XML and JSON formats are integrated in the backend (S3Resource).

A good tool for testing the REST functionality is the RESTClient

Resources

Resources are data objects on the server.

Resources can have attributes and methods. Attribute values are stored in the data store of the application (usually the local database), while methods are provided by the controller function of the particular resource.

Attributes can be simple (=atomic types) or complex (=object types), e.g. in a "person" resource the "first_name" attribute is of an atomic type (=string), while the "address" attribute is of an object type (=pr_address). Complex attributes in S3 are also called Components.

Components are stored in separate database tables which reference the primary resource record.

Resources as well as their components and methods can be addressed individually by URL's.

URL Argument Lists

General URL format:

http://host/application/module/resource/<arguments>?<vars>

The argument list is interpreted as follows:

  • empty argument list = all records of the resource
  • <id> = a particular record of the resource
  • <id>/<component> = all records of a component of a particular record of the resource
  • <component>/<id> = a particular record of a component of the resource

If a <method> argument is added at the end of the argument list, the addressed resource(s) will be accessed with that method.

If no <id>s are specified in the URL while a <method> or a <component> is used, then the last used <id> for this resource will be recalled. This feature can be used for cascading or returning UI actions. The <id> memory can be explicitly cleared by sending a request to the primary resource with clear as method. At login/logout as well as after a delete action, the <id> memory is automatically cleared, of course. Component record ID's are not remembered.

You may pass both an <id> for the resource record and an <id> for a component record within the same URL (however, you don't have to) - which would produce an error message if these two records do not belong together.

The data exchange format of the request can be passed:

If multiple extensions are specified, the rightmost extension applies. Where ?format= is specified, it overrides any extensions.

Can now use UID record addressing instead of IDs:

To come:

Model

Example for definition of a component table in the model:

resource = "image"
tablename = "%s_%s" % (module, resource)
db.define_table(tablename,
                pr_pe_id,
                opt_pr_image_type,
                Field("title"),
                Field("image", "upload", autodelete=True),
                Field("description"),
                Field("comment"),
                *s3_meta_fields())

# Component
s3db.add_component(tablename,
                   multiple=True,
                   joinby="pr_pe_id",
                   deletable=True,
                   editable=True,
                   list_fields = ["id", "opt_pr_image_type", "image", "title", "description"])

where:

  • module is the prefix of the module in which the component is defined
  • resource is the name of the component (without the module prefix)
  • multiple indicates whether this is a N:1 (True) or 1:1 (False) join, defaults to True
  • joinby describes the join keys:
    • pass a single field name for natural joins (same key field in both tables)
    • pass a dictionary of tablename=fieldname pairs for primary/foreign key matching, in which:
      • tablename is the name of the respective primary table
      • fieldname the name of the foreign key in the component table that points to the id field in the primary table
      • e.g. joinby = dict(pr_person="person_id")
  • fields is a list of the fields in the component that shall appear in list views:
    • if omitted or set to None, all readable fields will be included

No definitions are required at the primary resource, just define the table as usual.

Controller

Define a controller action per resource to invoke the controller:

def person():
    crud.settings.delete_onvalidation = shn_pentity_ondelete
    return s3_rest_controller(module, "person",
        main="first_name",
        extra="last_name",
        rheader=lambda r: shn_pr_rheader(r,
                    tabs = [(T("Basic Details"), None),
                            (T("Images"), "image"),
                            (T("Identity"), "identity"),
                            (T("Address"), "address"),
                            (T("Contact Data"), "pe_contact"),
                            (T("Memberships"), "group_membership"),
                            (T("Presence Log"), "presence"),
                            (T("Subscriptions"), "pe_subscription")
                            ]),
                sticky=True)

The optional rheader argument helps you to display some information about the primary resource record in the view while operating on a component (e.g. the person's name and ID, when displaying a list of available images for this person). You may pass static content, or a function or lambda to produce content, which will be forwarded as rheader variable to the view.

If for rheader a function or lambda is specified, then it will be called with the current S3Request ("r") as the only argument. Otherwise, rheader is passed to the view as-is.

The tabs functionality as in the example is to be implemented by the respective rheader function. This can make use of the helper function shn_rheader_tabs(), which renders tabs into the rheader to enable easy navigation through the resource and its components.

The sticky tag is to specify whether a create/update form of a primary record (=not a component) automatically returns to the list view or not:

  • If rheader is specified, it is assumed True, i.e. the create/update form of the primary record will not return to the list view but redirect to the update form again (=it is "sticky"). You can override this behavior by setting sticky=False - and if you do so, no rheader (and no tabs) will be rendered in the views of the primary record either.
  • If no rheader is specified, sticky will be assumed False unless you specify otherwise, hence the create/update forms will return to the list view. An rheader will not be rendered anyway (since not specified).

Options

There are some options which can be set before invoking the REST controller:

def kit():
    "REST CRUD controller"
    response.s3.formats.pdf = URL(f="kit_export_pdf")
    response.s3.formats.xls = URL(f="kit_export_xls")
    if len(request.args) == 2:
        crud.settings.update_next = URL(f="kit_item", args=request.args[1])
    return s3_rest_controller(module, "kit", main="code", onaccept=lambda form: kit_total(form))

The response.s3.formats.pdf & response.s3.formats.xls provide the view/formats.html with an alternate URL to provide a customised version of the PDF/XLS output available when clicking on the icon (response.s3.formats.rss is also available).

def report_overdue():
    "Report on Overdue Invoices - those unpaid 30 days after receipt"
    response.title = T("Overdue Invoices")
    overdue = datetime.date.today() - timedelta(days=30)
    response.s3.filter = (db.fin_invoice.date_out==None) & (db.fin_invoice.date_in < overdue)
    s3.crud_strings.fin_invoice.title_list = response.title
    s3.crud_strings.fin_invoice.msg_list_empty = T("No Invoices currently overdue")
    return s3_rest_controller(module, "invoice", deletable=False, listadd=False)

The response.s3.filter provides a filter which is used in the list view to show the desired subset of records (note that the s3.crud_strings can also be customised - when done in the Controller like this, they are good for just this request).

Plug-In Resource Actions

You may plug in custom resource actions to shn_rest_controller, e.g. if you have a custom search function for a resource.

Example: This adds a search_simple method to the person resource, which calls the shn_pr_person_search_simple function:

# Plug into REST controller
s3db.set_method(module, "person", method="search_simple", action=shn_pr_person_search_simple )

Example: The following disables the create method for a resource

def restricted_method(r, **attr):                                                                                
    """Handy function for restricting access to certain methods """                                                           
    session.error = T("Restricted method")                                                                                    
    redirect(URL(f="index"))   

# Note the s3xrc.model.set_method usage below.
def inbox():                                                                                                                  
    " RESTlike CRUD controller for inbox"                                                                                     
    person = db(db.pr_person.uuid == auth.user.person_uuid).select(db.pr_person.id)                                             
    response.s3.filter = (db.msg_inbox.person_id == person[0].id)                                                              
    s3db.set_method(module, "inbox", method="create", action = restricted_method)                                      
    return s3_rest_controller(module, "inbox", listadd=False, editable = False)  

Arguments of set_method:

  • module = prefix of the module of the primary resource
  • resource = name of the primary resource (without module prefix)
  • component=None = tablename of the component (if this method applies to a component)
  • method=None = name of the method
  • action=None = the function or lambda to invoke for that method (to remove a plug-in action, just pass None here)

The action function has to take the following arguments:

  • r = an instance of S3RESTRequest containing all data about the request
  • attr = a dictionary of named arguments (same named args as passed to the s3_rest_controller)

DeveloperGuidelines

Note: See TracWiki for help on using the wiki.