wiki:DeveloperGuidelines/Tutorial/RESTCustomisation

S3REST Customization

It is possible to extend the S3 RESTful API with custom methods.

An Example

We want a RESTful method to return a list of available shelters that fit for a certain group of people.

This is the URL to request the list of available shelters for a group:

  • GET eden/pr/group/5/available_shelters

This tutorial introduces the framework to implement such a method.

What is a Method Handler?

When a REST request arrives at the API, it is dispatched to a function that executes the requested HTTP method for the record(s) addressed by the URL.

In our example:

  • GET eden/pr/group/5

...the object is the pr_group record number 5 - and the requested method is HTTP GET.

The HTTP GET method, without any other arguments in the URL, would simply return the requested record.

We can specify additional arguments for the GET function to tell the API exactly what kind of information about this record we would like to get:

  • GET eden/pr/group/5/available_shelters

Now the GET API function will try to find a method handler, i.e. a function or class to deliver the available_shelters information for pr_group number 5.

There is a number of standard method handlers available globally for all types of records, e.g. for CRUD (create, read, update, delete) or to generate certain types of reports (report, profile).

Besides those standard methods, we can implement our own custom method handler for a particular table.

Implementing a Method Handler

Such a method handler can be either a function or a class:

def available_shelters_handler(r, **attr):
    """
        Method handler for the "available_shelters" method

        @param r: the S3Request
        @param attr: additional keyword parameters passed from the controller
    """

    # do something to produce output

    return output

or:

class AvailableShelters(S3Method):
    """
        Method handler for the "available_shelters" method
    """

    def apply_method(self, r, **attr):
        """
            Entry point for the RESTful API (=this function will be called to handle the request)

            @param r: the S3Request
            @param attr: additional keyword parameters passed from the controller
        """

        # do something to produce output

        return output

Note: It is recommended to use the S3Method pattern because the S3Method class automatically takes care of certain additional controller parameters (in **attr, e.g. rheader) - whereas in a function we would have to implement those ourselves.

The output from the method handler (that is, the list of available shelters in our example) is then handed over to the view renderer, and from there returned to the client.

Where is the Request Information?

Of course, the same method handler could also handle other types of request, e.g. when the user subsequently selects one of the shelters in the list in order to allocate it to the group number 5, then this would become:

  • POST eden/pr/group/5/available_shelters

Now it is the POST function calling our method handler, and to find out whether this is GET or POST, we can introspect the request object:

class AvailableShelters(S3Method):

    def apply_method(self, r, **attr):

        # Use r.http to find the HTTP method of the request:

        if r.http == "GET":
            # produce the list of available shelters

            ...

            # The requested record (pr_group 5) can be accessed like this:
            # (note: r.record could be None if the record is not accessible or doesn't exist)
            record = r.record 

            # Similar, the ID of the requested record can be found in:
            record_id = r.id

            # Being a S3Request instance, r contains a number of other important details
            # to introspect the request, the requested resource, as well as useful helper
            # functions for processing it
            # Check: http://eden.sahanafoundation.org/wiki/S3/S3Request

            ...

            return list_of_available_shelters

        elif r.http == "POST":
            # process the allocation of a shelter to this group

            ...

            # After the process, we would typically redirect to the next step
            # in the workflow, or return to the same view in case of failure.
            # In either case, we would show a message.
            # In S3Method subclasses we use self.next to specify the redirection
            # destination:

            if allocation_success:
                current.response.confirmation = T("Shelter allocated")
                self.next = r.url(method="") # <= redirect e.g. to the pr_group view
            else:
                # Allocation has failed - return to the GET view?
                current.response.error = T("Allocation failed")
                self.next = r.url() # <= redirect to the same URL, but with HTTP GET

            return # <= irrelevant what we return here, since we redirect anyway

How to Authorize the Method

When we implement a method handler that returns information from the database to the user, then we must check whether the user has permission to see this information:

    if current.auth.s3_has_permission("read", "cr_shelter"):
        # User is permitted to read in the shelter table

        # Use this pattern to get a query that extracts only those records the user has permission to read:
        table = current.s3db.cr_shelter
        accessible_query = current.auth.accessible_query("read", "cr_shelter")

        # This will return only the "accessible" rows
        rows = current.db(accessible_query).select(table.id,
                                                   table.name)

        # NB: if we intend to subsequently allow the user to allocate shelter capacity, we may also want to
        #     check whether the user is actually permitted both to read *and* to update the shelter information:
        accessible_query = current.auth.accessible_query(["read", "update"], "cr_shelter")


    else:
        # Easy to tell that they are not permitted to read in the shelter table:

        # If the user is logged in, this shows an "Insufficient Permissions" error to the user and
        # redirects to a landing page. If the user is not logged in, it will redirect to the login
        # page (however, alternative handling is possible - and sometimes useful and more user-friendly):
        r.unauthorized() 

Note that "read" permission for the requested record (pr_group 5) is already checked before the method handler is called, so it does not need to be checked again. But for any other record from which we return information to the user, or any other method we intend to perform, we must first check permissions in the method handler.

Configuring the Method Handler

Eventually, we need to tell the REST API which function/class to call for the available_shelters qualifier:

class S3GroupModel(S3Model):
    """ Model class for pr/group """

    def model(self):

        tablename = "pr_group"
        table = self.define_table(tablename,
                                  ...
                                  *s3_metafields())

        # Configure the custom method:
        # the "method" parameter defines the URL parameter needed to request this method (the method "name")

        self.set_method("pr", "group", method="available_shelters", action=AvailableShelters)

        ...

This can be done both in the model (like above, so it is available in all controllers), or in just the controller that needs it.

Note that if the method handler is implemented as S3Method subclass, we do not need to instantiate it. It will be instantiated only when it is needed to process the request.

Put it on a Tab

Now we want to see the list of available shelters on a component tab.

Component tabs are typically defined in the rheader function for the target table, so we check our rheader for pr_group:

    ...
    elif resourcename == "group":

        rheader_fields = [["name"],
                          ["description"],
                         ]

        tabs = [("Group Details", None),
                (T("Contact Data"), "contact"),
                (T("Members"), "group_membership"),

                # We add our custom method here:
                (T("Find Shelter"), "available_shelters"),

                ]
    ...

That's it ;)

Last modified 11 years ago Last modified on 03/03/14 21:05:48
Note: See TracWiki for help on using the wiki.