[[TOC]] = REST Controller = Available since: [https://code.launchpad.net/~flavour/sahana/sahanapy-trunk Main Trunk] Revision 358+ The "REST Controller" is a S3 framework component that provides a generic RESTful API for !SahanaPy database resources. The main function of the REST Controller is: * '''shn_rest_controller''' (defined in models/01_RESTlike_controller.py) The controller supports a number of representation formats: * HTML (including autogenerated Create/Update forms) * PDF (export only) * XLS (export only) * CSV * [wiki:S3XRC XML] using in-line XSLT transformation to support various XML formats * [wiki:S3XRC JSON] as light-weight alternative for AJAX-style resource access == Joined Resources == ''(This extends the single-table [wiki:BluePrintRESTImplementation REST Implementation])'' A '''joining request''' is a request to a table ('''component''') in dependency of a join to another table ('''resource'''), e.g.: * the list of addresses (=component) of a person (=resource) A resource and its components together represent a "Joined Resource". There are different ways how such joined resources can be seen: - as database tables with embedded subtables - as data objects with structured properties - as compound objects with component classes In this implementation, component tables can be joined 1:1 or N:1 to their primary resources, either by natural joins (same key field in both tables) or primary/foreign key matches, where the primary key is always the 'id' field in the primary table. == URL Argument Lists == General URL format: {{{ http://host/application/module/resource/? }}} The argument list is interpreted as follows: * '''empty argument list''' = access to all/any records of the resource * '''''' = access to record # of the resource * '''''' = ''method'' access to all/any records of the resource * '''/''' = ''method'' access to the record # of the resource * '''/''' = access to all/any component records of the record # of the resource * '''//''' = ''method'' access to all/any component records for the record # of the resource If '''''' is omitted from the URL while a or a is specified, then the last used for this resource will be recalled. This feature can be used for cascading or returning UI actions. The 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 memory is automatically cleared, of course. You may pass a record ID for the component at the end of the arguments list to access a particular component record - which would produce an error message if these two records do not belong together. Component record ID's are not remembered. The data format of the request can be passed: * as extension to the resource, e.g. http://localhost:8000/sahana/pr/person.xml/1 * as extension to any of the arguments, e.g. http://localhost:8000/sahana/pr/person/1/address/create.xml * as explicit variable "format", e.g. http://localhost:8000/sahana/pr/person/1/address?format=xml If multiple extensions are specified, the rightmost extension applies. Where ''?format='' is specified, it overrides any extensions. == Model == Example for definition of a component table in the model: {{{ resource = 'image' table = module + '_' + resource db.define_table(table, timestamp, uuidstamp, deletion_status, pr_pe_id, opt_pr_image_type, Field('title'), Field('image', 'upload', autodelete=True), Field('description'), Field('comment'), migrate=migrate) # Component s3xrc.model.add_component(module, resource, 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 == As usual: {{{ def person(): crud.settings.delete_onvalidation = shn_pentity_ondelete return shn_rest_controller(module, 'person', main='first_name', extra='last_name', pheader=shn_pr_pheader, onvalidation=lambda form: shn_pentity_onvalidation(form, table='pr_person', entity_class=1), onaccept=None) }}} New: the optional '''pheader''' argument. This 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 ''pheader'' variable to the view. If you pass a function or lambda as pheader, then it has to take 5 arguments: - '''resource''' = name of the primary resource - '''record_id''' = id of the primary resource record - '''representation''' = data format of the request - '''next=None''' = backlink URL to reproduce the request (with empty method) - '''same=None''' = backlink URL to reproduce the request (with empty method and containing the string '[id]' instead of the primary resource record id) The backlinks can be used to reproduce the original request after doing something on the primary resource (e.g., edit or change the selected record). * '''NOTE:''' Callbacks from CRUD settings (defined as in the example above) as well as onvalidation and onaccept callbacks are only invoked at requests on the main resource, but not at joined requests. To get callbacks invoked on Joining Requests, you need to set the ''onvalidation'' and ''onaccept'' attributes in the {{{s3xrc.model.add_component()}}} definition, or you set them later using {{{s3xrc.model.set_attr(component, function)}}} === Options === There are some options which can be set before invoking the REST controller: {{{ def kit(): "RESTlike CRUD controller" response.s3.pdf = URL(r=request, f='kit_export_pdf') response.s3.xls = URL(r=request, f='kit_export_xls') if len(request.args) == 2: crud.settings.update_next = URL(r=request, f='kit_item', args=request.args[1]) return shn_rest_controller(module, 'kit', main='code', onaccept=lambda form: kit_total(form)) }}} The {{{response.s3.pdf}}} & {{{response.s3.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.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 shn_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 jrlayer.set_method(module, 'person', None, None, 'search_simple', shn_pr_person_search_simple ) }}} Arguments of jrlayer.set_method: * '''module''' = name of the module of the primary resource * '''resource''' = name of the primary resource * '''jmodule''' = name of the module of the joined resource (if any, maybe None) * '''jresource''' = name of the joined resource (if any, maybe None) * '''method''' = name of the method * '''action''' = the function or lambda to invoke for that method (to remove a plug-in action, just pass None here) The action method in turn has to take plenty of arguments: * '''module''' * '''resource''' * '''record_id''' * '''method''' * '''jmodule=None''' * '''jresource=None''' * '''jrecord_id=None''' * '''joinby=None''' * '''multiple=True''' * '''representation="html"''' * '''onvalidation=None''' * '''onaccept=None'''