Changes between Version 13 and Version 14 of DeveloperGuidelines/Tutorial/RESTCustomisation


Ignore:
Timestamp:
03/03/14 20:33:43 (8 years ago)
Author:
Dominic König
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • DeveloperGuidelines/Tutorial/RESTCustomisation

    v13 v14  
    11[[TOC]]
    2 = S3REST Customisation Mini Tutorials =
    3 
    4 see also: [wiki:RESTController RESTController], [wiki:S3REST S3REST]
    5 
    6 == S3REST and CRUD Method Handlers ==
    7 
    8 It seems generally difficult for new developers to understand how to take control of functionality in the [wiki:RESTController] of the S3 framework.
    9 
    10 IMHO that is because most of the examples look like that:
    11 {{{
    12 def myresource():
    13 
    14    """ RESTful CRUD controller """
    15 
    16    return shn_rest_controller(module, resource)
    17 }}}
    18 This in fact leaves it all to the shn_rest_controller function - hard to see what really happens.
    19 
    20 However, shn_rest_controller does not actually mean you would be tied to the CRUD method handlers of {{{01_crud.py}}}. REST merely cares for the proper resolution of URLs and HTTP methods, and then calls hooked-in functions to process the requests, and hooks can be manipulated.
    21 
    22 First of all - shn_rest_controller returns some output:
    23 {{{
    24 def myresource():
    25 
    26    """ RESTful CRUD controller """
    27 
    28    output = shn_rest_controller(module, resource)
    29    return output
    30 }}}
    31 
    32 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:
    33 {{{
    34 def myresource():
    35 
    36    """ RESTful CRUD controller """
    37 
    38    output = shn_rest_controller(module, resource)
    39    if isinstance(output, dict):
    40        form = output.get("form", None)
    41        if form:
    42            # Your code to manipulate the form goes here:
    43            ...
    44 
    45    return output
    46 }}}
    47 
    48 That also means, you can add more items to the output dict:
    49 {{{
    50 def myresource():
    51 
    52    """ RESTful CRUD controller """
    53 
    54    output = shn_rest_controller(module, resource)
    55    if isinstance(output, dict):
    56        form = output.get("form", None)
    57            if form:
    58                # Code to manipulate form goes here:
    59                ...
    60        myitem = "I can send this item to the view"
    61        output.update(myitem=myitem)
    62 
    63    return output
    64 }}}
    65 
    66 Secondly, the shn_rest_controller has multiple hooks, among others for CRUD method handlers. Look at how shn_rest_controller is actually defined:
    67 {{{
    68 def shn_rest_controller(module, resource, **attr):
    69 
    70    s3rest.set_handler("import_xml", import_xml)
    71    s3rest.set_handler("import_json", import_json)
    72    s3rest.set_handler("list", shn_list)
    73    s3rest.set_handler("read", shn_read)
    74    s3rest.set_handler("create", shn_create)
    75    s3rest.set_handler("update", shn_update)
    76    s3rest.set_handler("delete", shn_delete)
    77    s3rest.set_handler("search", shn_search)
    78    s3rest.set_handler("options", shn_options)
    79 
    80    output = s3rest(session, request, response, module, resource, **attr)
    81    return output
    82 }}}
    83 
    84 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.
    85 
    86 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 [wiki:S3REST] to find out when prep and postp hooks are invoked.
    87 
    88 A prep hook would allow you to change a handler configuration in certain situations, e.g. testing a URL variable:
    89 {{{
    90 def myresource():
    91 
    92    """ RESTful CRUD controller """
    93 
    94    # Define pre-processor as local function:
    95    def _prep(jr):
    96        mylist = jr.request.vars.get("mylist")
    97        if mylist:
    98            s3rest.set_handler("list", my_list_controller)
    99        return True # do not forget to return True!
    100 
    101    # Hook pre-processor into REST controller:
    102    response.s3.prep = _prep
    103 
    104    output = shn_rest_controller(module, resource)
    105    return output
    106 }}}
    107 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.
    108 
    109 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).
    110 
    111 A very important structure during prep and postp is the S3RESTRequest object (usually instantiated as "jr"). This object contains all necessary
    112 information to process the current REST request, and it is passed to both prep and postp. See [wiki:S3REST#S3RESTRequest] for a list of attributes and methods.
    113 
    114 == Custom methods ==
    115 
    116 Let me give you another recipe:
    117 
    118 Imagine you have a resource "warehouse" and want to implement a function "report" that generates a report about the current status of the warehouse.
    119 
    120 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:
    121 
    122   - '''!http://site.myserver.org/eden/wm/warehouse/report'''
    123 
    124 
    125 and is provided in several different data formats beyond HTML (the interactive view), let's say - XLS and XML:
    126 
    127   - '''!http://site.myserver.org/eden/wm/warehouse/report.xls'''
    128   - '''!http://site.myserver.org/eden/wm/warehouse/report.xml'''
    129 
    130 How to tackle this? Yes - you can use shn_rest_controller for this!
    131 
    132 This could be your "warehouse" CRUD controller:
    133 
    134 {{{
    135 def warehouse():
    136 
    137    """ RESTful CRUD controller """
    138 
    139    return shn_rest_controller(module, resource, ...)
    140 }}}
    141 
    142 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:
    143 
    144 {{{
    145 def warehouse():
    146 
    147     """ RESTful CRUD+Reports controller """
    148 
    149     # Plug warehouse_report into warehouse resource:
    150     s3xrc.model.set_method(module, resource, method="report", action=warehouse_report)
    151 
    152     return shn_rest_controller(module, resource, ...)
    153 
    154 def warehouse_report(jr, **attr):
    155 
    156     """ Warehouse report generator """
    157 
    158     # Code to produce the report goes here
    159     report = ...
    160 
    161     return report
    162 }}}
    163 
    164 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
    165 are then formatted as HTML in the view template:
    166 
    167 {{{
    168 def warehouse_report(jr, **attr):
    169 
    170     """ Warehouse report generator """
    171 
    172     # Code to produce the report items:
    173     title = T("Warehouse Report")
    174 
    175     # Assemble the report items in a dict:
    176     report = dict(title=title, ...)
    177     return report
    178 }}}
    179 
    180 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
    181 available to the view templates. That also means: do not use "jr" as key in that dict.
    182 
    183 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
    184 found in jr:
    185 
    186 {{{
    187 def warehouse_report(jr, **attr):
    188 
    189     """ Warehouse report generator """
    190 
    191     if jr.representation in ("html", "popup"):
    192         # Code to produce the report items:
    193         title = T("Warehouse Report")
    194 
    195         # Assemble the report items in a dict:
    196         report = dict(title=title, ...)
    197 
    198     elif jr.representation == "xls":
    199         # Code to produce the XLS report goes here
     2= S3REST Customization =
     3
     4It is possible to extend the S3 RESTful API with custom methods.
     5
     6== An Example ==
     7
     8We want a RESTful method to return a list of available shelters that fit for a certain group of people.
     9
     10This is the '''URL''' to request the list of available shelters for a group:
     11
     12  * GET eden/pr/group/5/'''available_shelters'''
     13
     14This tutorial introduces the framework to implement such a method.
     15
     16== What is a Method Handler? ==
     17
     18When 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.
     19
     20In our example:
     21
     22  * GET eden/pr/group/5
     23
     24...the object is the '''pr_group''' record number '''5''' - and the requested method is HTTP '''GET'''.
     25
     26The HTTP GET method, without any other arguments in the URL, would simply return the requested record.
     27
     28We 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:
     29
     30  * GET eden/pr/group/5/'''available_shelters'''
     31
     32Now 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''.
     33
     34There 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).
     35
     36Besides those standard methods, we can implement our own ''custom'' method handler for a particular table.
     37
     38== Implementing a Method Handler ==
     39
     40Such a '''method handler''' can be either a function or a class:
     41
     42{{{#!python
     43def available_shelters_handler(r, **attr):
     44    """
     45        Method handler for the "available_shelters" method
     46
     47        @param r: the S3Request
     48        @param attr: additional keyword parameters passed from the controller
     49    """
     50
     51    # do something to produce output
     52
     53    return output
     54}}}
     55
     56or:
     57
     58{{{
     59class AvailableShelters(S3Method):
     60    """
     61        Method handler for the "available_shelters" method
     62    """
     63
     64    def apply_method(self, r, **attr):
     65        """
     66            Entry point for the RESTful API (=this function will be called to handle the request)
     67
     68            @param r: the S3Request
     69            @param attr: additional keyword parameters passed from the controller
     70        """
     71
     72        # do something to produce output
     73
     74        return output
     75
     76}}}
     77
     78  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. the "rheader") - whereas in a function we would have to implement those ourselves.''
     79
     80The 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.
     81
     82== Where is the Request Information? ==
     83
     84Of 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:
     85
     86  * POST eden/pr/group/5/available_shelters
     87
     88Now 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:
     89
     90{{{#!python
     91class AvailableShelters(S3Method):
     92
     93    def apply_method(self, r, **attr):
     94
     95        if r.http == "GET":
     96            # produce the list of available shelters
     97
     98            ...
     99
     100            # The requested record (pr_group 5) can be accessed like this:
     101            # (note: r.record could be None if the record is not accessible or doesn't exist)
     102            record = r.record
     103
     104            # Similar, the ID of the requested record can be found in:
     105            record_id = r.id
     106
     107            ...
     108
     109            return list_of_available_shelters
     110
     111        elif r.http == "POST":
     112            # process the allocation of a shelter to this group
     113
     114            ...
     115
     116            # After the process, we would typically redirect to the next step
     117            # in the workflow, or return to the same view in case of failure.
     118            # In either case, we would show a message.
     119            # In S3Method subclasses we use self.next to specify the redirection
     120            # destination:
     121
     122            if allocation_success:
     123                current.response.confirmation = T("Shelter allocated")
     124                self.next = r.url(method="") # <= redirect e.g. to the pr_group view
     125            else:
     126                # Allocation has failed - return to the GET view?
     127                current.response.error = T("Allocation failed")
     128                self.next = r.url() # <= redirect to the same URL, but with HTTP GET
     129
     130            return # <= irrelevant what we return here, since we redirect anyway
     131}}}
     132
     133== How to Authorize a Method ==
     134
     135When we implement a method handler that returns information, we need to request permission to do so:
     136
     137{{{#!python
     138    if current.auth.s3_has_permission("read", "cr_shelter"):
     139        # User is permitted to read in the shelter table
     140
     141        # Use this pattern to get a query that extracts only those records the user has permission to read:
     142        table = current.s3db.cr_shelter
     143        accessible_query = current.auth.accessible_query("read", "cr_shelter")
     144
     145        # This will return only the "accessible" rows
     146        rows = current.db(accessible_query).select(table.id,
     147                                                   table.name)
     148
     149        # NB: if we intend to subsequently allow the user to allocate shelter capacity, we may also want to
     150        #     check whether the user is actually permitted both to read *and* to update the shelter information:
     151        accessible_query = current.auth.accessible_query(["read", "update"], "cr_shelter")
     152
     153
     154    else:
     155        # Easy to tell that they are not permitted to read in the shelter table:
     156        r.unauthorized()
     157}}}
     158
     159Note 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.
     160
     161But for any other record we intend to return to the user, we must check permissions explicitly in the method handler.
     162
     163Note that you must request permission for ''every'' table that you intend to return information about.
     164
     165=== S3Method Subclasses as Method Handlers ===
     166
     167Method handlers can also be defined as subclass of S3Method:
     168
     169{{{
     170class AvailableShelters(S3Method):
     171    """
     172        Method handler for the "available_shelters" method
     173    """
     174
     175    def apply_method(self, r, **attr):
     176        """
     177            Entry point for the RESTful API
     178
     179            @param r: the S3Request
     180            @param attr: additional keyword parameters passed from the controller
     181        """
     182
     183        # do something to produce output
     184
     185        return output
     186
     187}}}
     188
     189=== Configuring a Method Handler ===
     190
     191To configure a method handler for a resource, use the {{{set_method()}}} function in S3Model (current.s3db):
     192
     193{{{
     194class S3GroupModel(S3Model):
     195    """ Model class for pr/group """
     196
     197    def model(self):
     198
     199        tablename = "pr_group"
     200        table = self.define_table(tablename,
     201                                  ...
     202                                  *s3_metafields())
     203
     204        # Configure a custom method (with S3Method subclass)
     205        self.set_method("pr", "group", method="available_shelters", action=AvailableShelters)
     206
    200207        ...
    201208
    202     elif jr.representation in shn_xml_export_formats:
    203         # Code to produce the XML report goes here
    204         ...
    205 
    206     else:
    207         session.error = BADFORMAT
    208         redirect(URL(r=jr.request))
    209 
    210     return report
    211 }}}
    212 
    213 See [wiki:S3REST#S3RESTRequest S3RESTRequest] to find out more about "jr".
    214 
    215 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:
    216 
    217 {{{
    218 def warehouse_report(jr, **attr):
    219     ...
    220     elif jr.representation in shn_xml_export_formats:
    221         report = export_xml(xrequest)
    222     ...
    223 }}}
    224 
    225 Perhaps you want to add an RSS feed:
    226 
    227 {{{
    228 def warehouse_report(jr, **attr):
    229     ...
    230     elif jr.representation == "rss":
    231         # Code to produce the RSS report goes here
    232         report = ...
    233     ...
    234 }}}
    235 
    236 Ah - now I forgot to mention how you can get at the data in the warehouse_report example.
    237 
    238 Your implementation already supports a variety of URLs:
    239 
    240 This URL addresses the report for all warehouses:
    241   - '''!http://site.myserver.org/eden/wm/warehouse/report'''
    242 
    243 This URL addresses the report for the warehouse record with ID=1.
    244   - '''!http://site.myserver.org/eden/wm/warehouse/1/report'''
    245 
    246 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).
    247   - '''!http://site.myserver.org/eden/wm/warehouse/report.xls?warehouse.uid=123654278'''
    248 
    249 The S3RESTRequest provides the resource information to your warehouse_report function.
    250 
    251 In case a specific record has been requested, you can access it as:
    252 {{{
    253     record = jr.record
    254 }}}
    255 If jr.record is None, then the request is targeting all warehouse records, so you'd take:
    256 {{{
    257     table = jr.table
    258     records = db().select(table.ALL)
    259     for record in records:
    260        ...
    261 }}}
    262 instead.
    263 
    264 NOTE: representation in jr is always all lowercase, there is no differentiation between the ".XML" and ".xml" extension in the URL.
    265 
    266 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).
     209class AvailableShelters(S3Method):
     210    """
     211        Method handler for the "available_shelters" method
     212    """
     213
     214    def apply_method(self, r, **attr):
     215        """
     216            Entry point for the RESTful API
     217
     218            @param r: the S3Request
     219            @param attr: additional keyword parameters passed from the controller
     220        """
     221
     222        # do something to produce output
     223
     224        return output
     225
     226}}}
     227
     228This can be done both in the model (like above, so it is available in all controllers), or in just the controller that needs it:
     229
     230{{{
     231def group():
     232    """ RESTful Controller for pr/group """
     233
     234    # Configure the handler for pr/group/N/available_shelters
     235    s3db.set_method("pr", "group", method="available_shelters", action=available_shelters_handler)
     236
     237    return s3_rest_controller()
     238
     239def available_shelters_handler(r, **attr):
     240    """
     241        Method handler for the "available_shelters" method
     242
     243        @param r: the S3Request
     244        @param attr: additional keyword parameters passed from the controller
     245    """
     246
     247    # do something to produce output
     248
     249    return output
     250
     251}}}