Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon

Web Server Development

Save for later
  • 24 min read
  • 15 Apr 2016

article-image

In this article by Holger Brunn, Alexandre Fayolle, and Daniel Eufémio Gago Reis, the authors of the book, Odoo Development Cookbook, have discussed how to deploy the web server in Odoo.

In this article, we'll cover the following topics:

  • Make a path accessible from the network
  • Restrict access to web accessible paths
  • Consume parameters passed to your handlers
  • Modify an existing handler
  • Using the RPC API

(For more resources related to this topic, see here.)

Introduction

We'll introduce the basics of the web server part of Odoo in this article. Note that this article covers the fundamental pieces.

All of Odoo's web request handling is driven by the Python library werkzeug (http://werkzeug.pocoo.org). While the complexity of werkzeug is mostly hidden by Odoo's convenient wrappers, it is an interesting read to see how things work under the hood.

Make a path accessible from the network

In this recipe, we'll see how to make an URL of the form http://yourserver/path1/path2 accessible to users. This can either be a web page or a path returning arbitrary data to be consumed by other programs. In the latter case, you would usually use the JSON format to consume parameters and to offer you data.

Getting ready

We'll make use of a ready-made library.book model. We want to allow any user to query the full list of books. Furthermore, we want to provide the same information to programs via a JSON request.

How to do it…

We'll need to add controllers, which go into a folder called controllers by convention.

  1. Add a controllers/main.py file with the HTML version of our page:
    from openerp import http
    from openerp.http import request
    
    class Main(http.Controller):
      @http.route('/my_module/books', type='http', auth='none')
        def books(self):
          records = request.env['library.book'].
            sudo().search([])
          result = '<html><body><table><tr><td>'
          result += '</td></tr><tr><td>'.join(
            records.mapped('name'))
          result += '</td></tr></table></body></html>'
          return result

  2. Add a function to serve the same information in the JSON format
    @http.route('/my_module/books/json', type='json', 
      auth='none')
      def books_json(self):
        records = request.env['library.book'].
          sudo().search([])
        return records.read(['name'])

  3. Add the file controllers/__init__.py:
    from . import main

  4. Add controllers to your __init__.py addon:
    from . import controllers

    After restarting your server, you can visit /my_module/books in your browser and get presented with a flat list of book names. To test the JSON-RPC part, you'll have to craft a JSON request. A simple way to do that would be using the following command line to receive the output on the command line:

    curl -i -X POST -H "Content-Type: application/json" -d "{}" localhost:8069/my_module/books/json

    If you get 404 errors at this point, you probably have more than one database available on your instance. In this case, it's impossible for Odoo to determine which database is meant to serve the request. Use the --db-filter='^yourdatabasename$' parameter to force using exact database you installed the module in. Now the path should be accessible.

How it works…

The two crucial parts here are that our controller is derived from openerp.http.Controller and that the methods we use to serve content are decorated with openerp.http.route. Inheriting from openerp.http.Controller registers the controller with Odoo's routing system in a similar way as models are registered by inheriting from openerp.models.Model; also, Controller has a meta class that takes care of this.

In general, paths handled by your addon should start with your addon's name to avoid name clashes. Of course, if you extend some addon's functionality, you'll use this addon's name.

openerp.http.route

The route decorator allows us to tell Odoo that a method is to be web accessible in the first place, and the first parameter determines on which path it is accessible. Instead of a string, you can also pass a list of strings in case you use the same function to serve multiple paths.

The type argument defaults to http and determines what type of request is to be served. While strictly speaking JSON is HTTP, declaring the second function as type='json' makes life a lot easier, because Odoo then handles type conversions itself.

Don't worry about the auth parameter for now, it will be addressed in recipe Restrict access to web accessible paths.

Return values

Odoo's treatment of the functions' return values is determined by the type argument of the route decorator. For type='http', we usually want to deliver some HTML, so the first function simply returns a string containing it. An alternative is to use request.make_response(), which gives you control over the headers to send in the response. So to indicate when our page was updated the last time, we might change the last line in books() to the following:

return request.make_response(
  result, [
    ('Last-modified', email.utils.formatdate(
      (
        fields.Datetime.from_string(
        request.env['library.book'].sudo()
        .search([], order='write_date desc', limit=1)
        .write_date) -
        datetime.datetime(1970, 1, 1)
      ).total_seconds(),
      usegmt=True)),
])

This code sends a Last-modified header along with the HTML we generated, telling the browser when the list was modified for the last time. We extract this information from the write_date field of the library.book model.

In order for the preceding snippet to work, you'll have to add some imports on the top of the file:

import email
import datetime
from openerp import fields

You can also create a Response object of werkzeug manually and return that, but there's little gain for the effort.

Generating HTML manually is nice for demonstration purposes, but you should never do this in production code. Always use templates as appropriate and return them by calling request.render().

This will give you localization for free and makes your code better by separating business logic from the presentation layer. Also, templates provide you with functions to escape data before outputting HTML. The preceding code is vulnerable to cross-site-scripting attacks if a user manages to slip a script tag into the book name, for example.

For a JSON request, simply return the data structure you want to hand over to the client, Odoo takes care of serialization. For this to work, you should restrict yourself to data types that are JSON serializable, which are roughly dictionaries, lists, strings, floats and integers.

openerp.http.request

The request object is a static object referring to the currently handled request, which contains everything you need to take useful action. Most important is the property request.env, which contains an Environment object which is just the same as in self.env for models. This environment is bound to the current user, which is none in the preceding example because we used auth='none'. Lack of a user is also why we have to sudo() all our calls to model methods in the example code.

If you're used to web development, you'll expect session handling, which is perfectly correct. Use request.session for an OpenERPSession object (which is quite a thin wrapper around the Session object of werkzeug), and request.session.sid to access the session id. To store session values, just treat request.session as a dictionary:

request.session['hello'] = 'world'
request.session.get('hello')

Note that storing data in the session is not different from using global variables. Use it only if you must - that is usually the case for multi request actions like a checkout in the website_sale module. And also in this case, handle all functionality concerning sessions in your controllers, never in your modules.

There's more…

The route decorator can have some extra parameters to customize its behavior further. By default, all HTTP methods are allowed, and Odoo intermingles with the parameters passed. Using the parameter methods, you can pass a list of methods to accept, which usually would be one of either ['GET'] or ['POST'].

To allow cross origin requests (browsers block AJAX and some other types of requests to domains other than where the script was loaded from for security and privacy reasons), set the cors parameter to * to allow requests from all origins, or some URI to restrict requests to ones originating from this URI. If this parameter is unset, which is the default, the Access-Control-Allow-Origin header is not set, leaving you with the browser's standard behavior. In our example, we might want to set it on /my_module/books/json in order to allow scripts pulled from other websites accessing the list of books.

By default, Odoo protects certain types of requests from an attack known as cross-site request forgery by passing a token along on every request. If you want to turn that off, set the parameter csrf to False, but note that this is a bad idea in general.

See also

If you host multiple Odoo databases on the same instance and each database has different web accessible paths on possibly multiple domain names per database, the standard regular expressions in the --db-filter parameter might not be enough to force the right database for every domain. In that case, use the community module dbfilter_from_header from https://github.com/OCA/server-tools in order to configure the database filters on proxy level.

To see how using templates makes modularity possible, see recipe Modify an existing handler later in the article.

Restrict access to web accessible paths

We'll explore the three authentication mechanisms Odoo provides for routes in this recipe. We'll define routes with different authentication mechanisms in order to show their differences.

Getting ready

As we extend code from the previous recipe, we'll also depend on the library.book model, so you should get its code correct in order to proceed.

How to do it…

Define handlers in controllers/main.py:

  1. Add a path that shows all books:
    @http.route('/my_module/all-books', type='http', 
      auth='none')
      def all_books(self):
        records = request.env['library.book'].sudo().search([])
        result = '<html><body><table><tr><td>'
        result += '</td></tr><tr><td>'.join(
          records.mapped('name'))
        result += '</td></tr></table></body></html>'
        return result

  2. Add a path that shows all books and indicates which was written by the current user, if any:
    @http.route('/my_module/all-books/mark-mine', 
      type='http', auth='public')
    def all_books_mark_mine(self):
      records = request.env['library.book'].sudo().search([])
      result = '<html><body><table>'
      for record in records:
      result += '<tr>'
      if record.author_ids & request.env.user.partner_id:
        result += '<th>'
      else:
        result += '<td>'
        result += record.name
        if record.author_ids & request.env.user.partner_id:
          result += '</th>'
        else:
          result += '</td>'
          result += '</tr>'
          result += '</table></body></html>'
          return result

  3. Add a path that shows the current user's books:
    @http.route('/my_module/all-books/mine', type='http', 
      auth='user')
      def all_books_mine(self):
        records = request.env['library.book'].search([
          ('author_ids', 'in', 
            request.env.user.partner_id.ids),
        ])
        result = '<html><body><table><tr><td>'
        result += '</td></tr><tr><td>'.join(
          records.mapped('name'))
        result += '</td></tr></table></body></html>'
        return result

    With this code, the paths /my_module/all_books and /my_module/all_books/mark_mine look the same for unauthenticated users, while a logged in user sees her books in a bold font on the latter path. The path /my_module/all-books/mine is not accessible at all for unauthenticated users. If you try to access it without being authenticated, you'll be redirected to the login screen in order to do so.

How it works…

The difference between authentication methods is basically what you can expect from the content of request.env.user.

For auth='none', the user record is always empty, even if an authenticated user is accessing the path. Use this if you want to serve content that has no dependencies on users, or if you want to provide database agnostic functionality in a server wide module.

The value auth='public' sets the user record to a special user with XML ID, base.public_user, for unauthenticated users, and to the user's record for authenticated ones. This is the right choice if you want to offer functionality to both unauthenticated and authenticated users, while the authenticated ones get some extras, as demonstrated in the preceding code.

Use auth='user' to be sure that only authenticated users have access to what you've got to offer. With this method, you can be sure request.env.user points to some existing user.

There's more…

The magic for authentication methods happens in the ir.http model from the base addon. For whatever value you pass to the auth parameter in your route, Odoo searches for a function called _auth_method_<yourvalue> on this model, so you can easily customize this by inheriting this model and declaring a method that takes care of your authentication method of choice.

As an example, we provide an authentication method base_group_user which enforces a currently logged in user who is a member of the group with XML ID, base.group_user:

from openerp import exceptions, http, models
from openerp.http import request

class IrHttp(models.Model):
  _inherit = 'ir.http'

  def _auth_method_base_group_user(self):
    self._auth_method_user()
    if not request.env.user.has_group('base.group_user'):
      raise exceptions.AccessDenied()

Now you can say auth='base_group_user' in your decorator and be sure that users running this route's handler are members of this group. With a little trickery you can extend this to auth='groups(xmlid1,…)', the implementation of this is left as an exercise to the reader, but is included in the example code.

Unlock access to the largest independent learning library in Tech for FREE!
Get unlimited access to 7500+ expert-authored eBooks and video courses covering every tech area you can think of.
Renews at €18.99/month. Cancel anytime

Consume parameters passed to your handlers

It's nice to be able to show content, but it's better to show content as a result of some user input. This recipe will demonstrate the different ways to receive this input and react to it. As the recipes before, we'll make use of the library.book model.

How to do it…

First, we'll add a route that expects a traditional parameter with a book's ID to show some details about it. Then, we'll do the same, but we'll incorporate our parameter into the path itself:

  1. Add a path that expects a book's ID as parameter:
    @http.route('/my_module/book_details', type='http', 
      auth='none')
    def book_details(self, book_id):
      record = request.env['library.book'].sudo().browse(
        int(book_id))
    return u'<html><body><h1>%s</h1>Authors: %s' % (
      record.name, u', '.join(record.author_ids.mapped(
        'name')) or 'none',
    )

  2. Add a path where we can pass the book's ID in the path
    @http.route("/my_module/book_details/<model('library.book')
      :book>", type='http', auth='none')
    def book_details_in_path(self, book):
      return self.book_details(book.id)

If you point your browser to /my_module/book_details?book_id=1, you should see a detail page of the book with ID 1. If this doesn't exist, you'll receive an error page.

The second handler allows you to go to /my_module/book_details/1 and view the same page.

How it works…

By default, Odoo (actually werkzeug) intermingles with GET and POST parameters and passes them as keyword argument to your handler. So by simply declaring your function as expecting a parameter called book_id, you introduce this parameter as either GET (the parameter in the URL) or POST (usually passed by forms with your handler as action) parameter. Given that we didn't add a default value for this parameter, the runtime will raise an error if you try to access this path without setting the parameter.

The second example makes use of the fact that in a werkzeug environment, most paths are virtual anyway. So we can simply define our path as containing some input. In this case, we say we expect the ID of a library.book as the last component of the path. The name after the colon is the name of a keyword argument. Our function will be called with this parameter passed as keyword argument. Here, Odoo takes care of looking up this ID and delivering a browse record, which of course only works if the user accessing this path has appropriate permissions. Given that book is a browse record, we can simply recycle the first example's function by passing book.id as parameter book_id to give out the same content.

There's more…

Defining parameters within the path is a functionality delivered by werkzeug, which is called converters. The model converter is added by Odoo, which also defines the converter, models, that accepts a comma separated list of IDs and passes a record set containing those IDs to your handler.

The beauty of converters is that the runtime coerces the parameters to the expected type, while you're on your own with normal keyword parameters. These are delivered as strings and you have to take care of the necessary type conversions yourself, as seen in the first example.

Built-in werkzeug converters include int, float, and string, but also more intricate ones such as path, any, or uuid. You can look up their semantics at http://werkzeug.pocoo.org/docs/0.11/routing/#builtin-converters.

See also

Odoo's custom converters are defined in ir_http.py in the base module and registered in the _get_converters method of ir.http. As an exercise, you can create your own converter that allows you to visit the /my_module/book_details/Odoo+cookbook page to receive the details of this book (if you added it to your library before).

Modify an existing handler

When you install the website module, the path /website/info displays some information about your Odoo instance. In this recipe, we override this in order to change this information page's layout, but also to change what is displayed.

Getting ready

Install the website module and inspect the path /website/info. Now craft a new module that depends on website and uses the following code.

How to do it…

We'll have to adapt the existing template and override the existing handler:

  1. Override the qweb template in a file called views/templates.xml:
    <?xml version="1.0" encoding="UTF-8"?>
    <odoo>
      <template id="show_website_info" 
        inherit_id="website.show_website_info">
        <xpath expr="//dl[@t-foreach='apps']" 
          position="replace">
          <table class="table">
            <tr t-foreach="apps" t-as="app">
              <th>
                <a t-att-href="app.website">
                <t t-esc="app.name" /></a>
              </th>
              <td><t t-esc="app.summary" /></td>
            </tr>
          </table>
        </xpath>
      </template>
    </odoo>

  2. Override the handler in a file called controllers/main.py:
    from openerp import http
    from openerp.addons.website.controllers.main import Website
    
    class Website(Website):
      @http.route()
      def website_info(self):
        result = super(Website, self).website_info()
        result.qcontext['apps'] = result.qcontext[
          'apps'].filtered(
          lambda x: x.name != 'website')
          return result

Now when visiting the info page, we'll only see a filtered list of installed applications, and in a table as opposed to the original definition list.

How it works

In the first step, we override an existing QWeb template. In order to find out which that is, you'll have to consult the code of the original handler. Usually, it will end with the following command line, which tells you that you need to override template.name:

return request.render('template.name', values)

In our case, the handler uses a template called website.info, but this one is extended immediately by another template called website.show_website_info, so it's more convenient to override this one. Here, we replace the definition list showing installed apps with a table.

In order to override the handler method, we must identify the class that defines the handler, which is openerp.addons.website.controllers.main.Website in this case. We import the class to be able to inherit from it. Now we override the method and change the data passed to the response. Note that what the overridden handler returns is a Response object and not a string of HTML as the previous recipes did for the sake of brevity. This object contains a reference to the template to be used and the values accessible to the template, but is only evaluated at the very end of the request.

In general, there are three ways to change an existing handler:

  • If it uses a QWeb template, the simplest way of changing it is to override the template. This is the right choice for layout changes and small logic changes.
  • QWeb templates get a context passed, which is available in the response as the field qcontext. This usually is a dictionary where you can add or remove values to suit your needs. In the preceding example, we filter the list of apps to only contain apps which have a website set.
  • If the handler receives parameters, you could also preprocess those in order to have the overridden handler behave the way you want.

There's more…

As seen in the preceding section, inheritance with controllers works slightly differently than model inheritance: You actually need a reference to the base class and use Python inheritance on it.

Don't forget to decorate your new handler with the @http.route decorator; Odoo uses it as a marker for which methods are exposed to the network layer. If you omit the decorator, you actually make the handler's path inaccessible.

The @http.route decorator itself behaves similarly to field declarations: every value you don't set will be derived from the decorator of the function you're overriding, so we don't have to repeat values we don't want to change.

After receiving a response object from the function you override, you can do a lot more than just changing the QWeb context:

  • You can add or remove HTTP headers by manipulating response.headers.
  • If you want to render an entirely different template, you can set response.template.
  • To detect if a response is based on QWeb in the first place, query response.is_qweb.
  • The resulting HTML code is available by calling response.render().

Using the RPC API

One of Odoo's strengths is its interoperability, which is helped by the fact that basically any functionality is available via JSON-RPC 2.0 and XMLRPC. In this recipe, we'll explore how to use both of them from client code. This interface also enables you to integrate Odoo with any other application. Making functionality available via any of the two protocols on the server side is explained in the There's more section of this recipe.

We'll query a list of installed modules from the Odoo instance, so that we could show a list as the one displayed in the previous recipe in our own application or website.

How to do it…

The following code is not meant to run within Odoo, but as simple scripts:

  1. First, we query the list of installed modules via XMLRPC:
    #!/usr/bin/env python2
    import xmlrpclib
    
    db = 'odoo9'
    user = 'admin'
    password = 'admin'
    uid = xmlrpclib.ServerProxy(
      'http://localhost:8069/xmlrpc/2/common')
        .authenticate(db, user, password, {})
    odoo = xmlrpclib.ServerProxy(
      'http://localhost:8069/xmlrpc/2/object')
    installed_modules = odoo.execute_kw(
      db, uid, password, 'ir.module.module', 'search_read',
      [[('state', '=', 'installed')], ['name']], {'context': 
        {'lang': 'fr_FR'}})
    for module in installed_modules:
      print module['name']

  2. Then we do the same with JSONRPC:
    import json
    import urllib2
    
    db = 'odoo9'
    user = 'admin'
    password = 'admin'
    
    request = urllib2.Request(
      'http://localhost:8069/web/session/authenticate',
        json.dumps({
          'jsonrpc': '2.0',
            'params': {
              'db': db,
              'login': user,
              'password': password,
            },
        }),
        {'Content-type': 'application/json'})
        result = urllib2.urlopen(request).read()
        result = json.loads(result)
        session_id = result['result']['session_id']
        request = urllib2.Request(
          'http://localhost:8069/web/dataset/call_kw',
          json.dumps({
            'jsonrpc': '2.0',
            'params': {
              'model': 'ir.module.module',
              'method': 'search_read',
              'args': [
                [('state', '=', 'installed')],
                ['name'],
              ],
              'kwargs': {'context': {'lang': 'fr_FR'}},
            },
        }),
        {
            'X-Openerp-Session-Id': session_id,
            'Content-type': 'application/json',
      })
      result = urllib2.urlopen(request).read()
      result = json.loads(result)
      for module in result['result']:
        print module['name']

Both code snippets will print a list of installed modules, and because they pass a context that sets the language to French, the list will be in French if there are no translations available.

How it works…

Both snippets call the function search_read, which is very convenient because you can specify a search domain on the model you call, pass a list of fields you want to be returned, and receive the result in one request. In older versions of Odoo, you had to call search first to receive a list of IDs and then call read to actually read the data.

search_read returns a list of dictionaries, with the keys being the names of the fields requested and the values the record's data. The ID field will always be transmitted, no matter if you requested it or not.

Now, we need to look at the specifics of the two protocols.

XMLRPC

The XMLRPC API expects a user ID and a password for every call, which is why we need to fetch this ID via the method authenticate on the path /xmlrpc/2/common. If you already know the user's ID, you can skip this step.

As soon as you know the user's ID, you can call any model's method by calling execute_kw on the path /xmlrpc/2/object. This method expects the database you want to execute the function on, the user's ID and password for authentication, then the model you want to call your function on, and then the function's name. The next two mandatory parameters are a list of positional arguments to your function, and a dictionary of keyword arguments.

JSONRPC

Don't be distracted by the size of the code example, that's because Python doesn't have built in support for JSONRPC. As soon as you've wrapped the urllib calls in some helper functions, the example will be as concise as the XMLRPC one.

As JSONRPC is stateful, the first thing we have to do is to request a session at /web/session/authenticate. This function takes the database, the user's name, and their password.

The crucial part here is that we record the session ID Odoo created, which we pass in the header X-Openerp-Session-Id to /web/dataset/call_kw. Then the function behaves the same as execute_kw from; we need to pass a model name and a function to call on it, then positional and keyword arguments.

There's more…

Both protocols allow you to call basically any function of your models. In case you don't want a function to be available via either interface, prepend its name with an underscore – Odoo won't expose those functions as RPC calls.

Furthermore, you need to take care that your parameters, as well as the return values, are serializable for the protocol. To be sure, restrict yourself to scalar values, dictionaries, and lists.

As you can do roughly the same with both protocols, it's up to you which one to use. This decision should be mainly driven by what your platform supports best. In a web context, you're generally better off with JSON, because Odoo allows JSON handlers to pass a CORS header conveniently (see the Make a path accessible from the network recipe for details). This is rather difficult with XMLRPC.

Summary

In this article, we saw how to start about with the web server architecture. Later on, we covered the Routes and Controllers that will be used in the article and their authentication, how the handlers consumes parameters, and how to use an RPC API, namely, JSON-RPC and XML-RPC.

Resources for Article:


Further resources on this subject: