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 | |
| 4 | It is possible to extend the S3 RESTful API with custom methods. |
| 5 | |
| 6 | == An Example == |
| 7 | |
| 8 | We want a RESTful method to return a list of available shelters that fit for a certain group of people. |
| 9 | |
| 10 | This is the '''URL''' to request the list of available shelters for a group: |
| 11 | |
| 12 | * GET eden/pr/group/5/'''available_shelters''' |
| 13 | |
| 14 | This tutorial introduces the framework to implement such a method. |
| 15 | |
| 16 | == What is a Method Handler? == |
| 17 | |
| 18 | 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. |
| 19 | |
| 20 | In 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 | |
| 26 | The HTTP GET method, without any other arguments in the URL, would simply return the requested record. |
| 27 | |
| 28 | 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: |
| 29 | |
| 30 | * GET eden/pr/group/5/'''available_shelters''' |
| 31 | |
| 32 | 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''. |
| 33 | |
| 34 | 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). |
| 35 | |
| 36 | Besides those standard methods, we can implement our own ''custom'' method handler for a particular table. |
| 37 | |
| 38 | == Implementing a Method Handler == |
| 39 | |
| 40 | Such a '''method handler''' can be either a function or a class: |
| 41 | |
| 42 | {{{#!python |
| 43 | def 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 | |
| 56 | or: |
| 57 | |
| 58 | {{{ |
| 59 | class 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 | |
| 80 | 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. |
| 81 | |
| 82 | == Where is the Request Information? == |
| 83 | |
| 84 | 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: |
| 85 | |
| 86 | * POST eden/pr/group/5/available_shelters |
| 87 | |
| 88 | 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: |
| 89 | |
| 90 | {{{#!python |
| 91 | class 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 | |
| 135 | When 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 | |
| 159 | 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. |
| 160 | |
| 161 | But for any other record we intend to return to the user, we must check permissions explicitly in the method handler. |
| 162 | |
| 163 | Note that you must request permission for ''every'' table that you intend to return information about. |
| 164 | |
| 165 | === S3Method Subclasses as Method Handlers === |
| 166 | |
| 167 | Method handlers can also be defined as subclass of S3Method: |
| 168 | |
| 169 | {{{ |
| 170 | class 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 | |
| 191 | To configure a method handler for a resource, use the {{{set_method()}}} function in S3Model (current.s3db): |
| 192 | |
| 193 | {{{ |
| 194 | class 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 | |
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). |
| 209 | class 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 | |
| 228 | 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: |
| 229 | |
| 230 | {{{ |
| 231 | def 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 | |
| 239 | def 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 | }}} |