[[TOC]] = 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: {{{#!python 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: {{{#!python 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: {{{#!python 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: {{{#!python 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: {{{#!python 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'': {{{#!python ... 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 ;)