diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/doc/module.rst | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/doc/module.rst')
| -rw-r--r-- | addons/web/doc/module.rst | 442 |
1 files changed, 442 insertions, 0 deletions
diff --git a/addons/web/doc/module.rst b/addons/web/doc/module.rst new file mode 100644 index 00000000..ca808254 --- /dev/null +++ b/addons/web/doc/module.rst @@ -0,0 +1,442 @@ +.. _module: + +.. queue:: module/series + +Building a Web module +===================== + +There is no significant distinction between a Web module and +a regular module, the web part is mostly additional data and code +inside a regular module. This allows providing more seamless +features by integrating your module deeper into the web client. + +A Basic Module +-------------- + +A very basic OpenERP module structure will be our starting point: + +.. code-block:: text + + web_example + ├── __init__.py + └── __manifest__.py + +.. patch:: + +This is a sufficient minimal declaration of a valid module. + +Web Declaration +--------------- + +There is no such thing as a "web module" declaration. An OpenERP +module is automatically recognized as "web-enabled" if it contains a +``static`` directory at its root, so: + +.. code-block:: text + + web_example + ├── __init__.py + ├── __manifest__.py + └── static + +is the extent of it. You should also change the dependency to list +``web``: + +.. patch:: + +.. note:: + + This does not matter in normal operation so you may not realize + it's wrong (the web module does the loading of everything else, so + it can only be loaded), but when e.g. testing the loading process + is slightly different than normal, and incorrect dependency may + lead to broken code. + +This makes the "web" discovery system consider the module as having a +"web part", and check if it has web controllers to mount or javascript +files to load. The content of the ``static/`` folder is also +automatically made available to web browser at the URL +``$module-name/static/$file-path``. This is sufficient to provide +pictures (of cats, usually) through your module. However there are +still a few more steps to running javascript code. + +Getting Things Done +------------------- + +The first one is to add javascript code. It's customary to put it in +``static/src/js``, to have room for e.g. other file types, or +third-party libraries. + +.. patch:: + +The client won't load any file unless specified, thus the new file +should be listed in the module's manifest file, under a new key ``js`` +(a list of file names, or glob patterns): + +.. patch:: + +At this point, if the module is installed and the client reloaded the +message should appear in your browser's development console. + +.. note:: + + Because the manifest file has been edited, you will have to + restart the OpenERP server itself for it to be taken in account. + + You may also want to open your browser's console *before* + reloading, depending on the browser messages printed while the + console is closed may not work or may not appear after opening it. + +.. note:: + + If the message does not appear, try cleaning your browser's caches + and ensure the file is correctly loaded from the server logs or + the "resources" tab of your browser's developers tools. + +At this point the code runs, but it runs only once when the module is +initialized, and it can't get access to the various APIs of the web +client (such as making RPC requests to the server). This is done by +providing a `javascript module`_: + +.. patch:: + +If you reload the client, you'll see a message in the console exactly +as previously. The differences, though invisible at this point, are: + +* All javascript files specified in the manifest (only this one so + far) have been fully loaded +* An instance of the web client and a namespace inside that instance + (with the same name as the module) have been created and are + available for use + +The latter point is what the ``instance`` parameter to the function +provides: an instance of the OpenERP Web client, with the contents of +all the new module's dependencies loaded in and initialized. These are +the entry points to the web client's APIs. + +To demonstrate, let's build a simple :doc:`client action +<client_action>`: a stopwatch + +First, the action declaration: + +.. patch:: + +then set up the :doc:`client action hook <client_action>` to register +a function (for now): + +.. patch:: + +Updating the module (in order to load the XML description) and +re-starting the server should display a new menu *Example Client +Action* at the top-level. Opening said menu will make the message +appear, as usual, in the browser's console. + +Paint it black +-------------- + +The next step is to take control of the page itself, rather than just +print little messages in the console. This we can do by replacing our +client action function by a :doc:`widget`. Our widget will simply use +its :js:func:`~openerp.web.Widget.start` to add some content to its +DOM: + +.. patch:: + +after reloading the client (to update the javascript file), instead of +printing to the console the menu item clears the whole screen and +displays the specified message in the page. + +Since we've added a class on the widget's :ref:`DOM root +<widget-dom_root>` we can now see how to add a stylesheet to a module: +first create the stylesheet file: + +.. patch:: + +then add a reference to the stylesheet in the module's manifest (which +will require restarting the OpenERP Server to see the changes, as +usual): + +.. patch:: + +the text displayed by the menu item should now be huge, and +white-on-black (instead of small and black-on-white). From there on, +the world's your canvas. + +.. note:: + + Prefixing CSS rules with both ``.openerp`` (to ensure the rule + will apply only within the confines of the OpenERP Web client) and + a class at the root of your own hierarchy of widgets is strongly + recommended to avoid "leaking" styles in case the code is running + embedded in an other web page, and does not have the whole screen + to itself. + +So far we haven't built much (any, really) DOM content. It could all +be done in :js:func:`~openerp.web.Widget.start` but that gets unwieldy +and hard to maintain fast. It is also very difficult to extend by +third parties (trying to add or change things in your widgets) unless +broken up into multiple methods which each perform a little bit of the +rendering. + +The first way to handle this method is to delegate the content to +plenty of sub-widgets, which can be individually overridden. An other +method [#DOM-building]_ is to use `a template +<http://en.wikipedia.org/wiki/Web_template>`_ to render a widget's +DOM. + +OpenERP Web's template language is :doc:`qweb`. Although any +templating engine can be used (e.g. `mustache +<http://mustache.github.com/>`_ or `_.template +<http://underscorejs.org/#template>`_) QWeb has important features +which other template engines may not provide, and has special +integration to OpenERP Web widgets. + +Adding a template file is similar to adding a style sheet: + +.. patch:: + +The template can then easily be hooked in the widget: + +.. patch:: + +And finally the CSS can be altered to style the new (and more complex) +template-generated DOM, rather than the code-generated one: + +.. patch:: + +.. note:: + + The last section of the CSS change is an example of "state + classes": a CSS class (or set of classes) on the root of the + widget, which is toggled when the state of the widget changes and + can perform drastic alterations in rendering (usually + showing/hiding various elements). + + This pattern is both fairly simple (to read and understand) and + efficient (because most of the hard work is pushed to the + browser's CSS engine, which is usually highly optimized, and done + in a single repaint after toggling the class). + +The last step (until the next one) is to add some behavior and make +our stopwatch watch. First hook some events on the buttons to toggle +the widget's state: + +.. patch:: + +This demonstrates the use of the "events hash" and event delegation to +declaratively handle events on the widget's DOM. And already changes +the button displayed in the UI. Then comes some actual logic: + +.. patch:: + +* An initializer (the ``init`` method) is introduced to set-up a few + internal variables: ``_start`` will hold the start of the timer (as + a javascript Date object), and ``_watch`` will hold a ticker to + update the interface regularly and display the "current time". + +* ``update_counter`` is in charge of taking the time difference + between "now" and ``_start``, formatting as ``HH:MM:SS`` and + displaying the result on screen. + +* ``watch_start`` is augmented to initialize ``_start`` with its value + and set-up the update of the counter display every 33ms. + +* ``watch_stop`` disables the updater, does a final update of the + counter display and resets everything. + +* Finally, because javascript Interval and Timeout objects execute + "outside" the widget, they will keep going even after the widget has + been destroyed (especially an issue with intervals as they repeat + indefinitely). So ``_watch`` *must* be cleared when the widget is + destroyed (then the ``_super`` must be called as well in order to + perform the "normal" widget cleanup). + +Starting and stopping the watch now works, and correctly tracks time +since having started the watch, neatly formatted. + +Burning through the skies +------------------------- + +All work so far has been "local" outside of the original impetus +provided by the client action: the widget is self-contained and, once +started, does not communicate with anything outside itself. Not only +that, but it has no persistence: if the user leaves the stopwatch +screen (to go and see his inbox, or do some well-deserved accounting, +for instance) whatever was being timed will be lost. + +To prevent this irremediable loss, we can use OpenERP's support for +storing data as a model, allowing so that we don't lose our data and +can later retrieve, query and manipulate it. First let's create a +basic OpenERP model in which our data will be stored: + +.. patch:: + +then let's add saving times to the database every time the stopwatch +is stopped, using :js:class:`the "high-level" Model API +<openerp.web.Model.call>`: + +.. patch:: + +A look at the "Network" tab of your preferred browser's developer +tools while playing with the stopwatch will show that the save +(creation) request is indeed sent (and replied to, even though we're +ignoring the response at this point). + +These saved data should now be loaded and displayed when first opening +the action, so the user can see his previously recorded times. This is +done by overloading the model's ``start`` method: the purpose of +:js:func:`~openerp.base.Widget.start()` is to perform *asynchronous* +initialization steps, so the rest of the web client knows to "wait" +and gets a readiness signal. In this case, it will fetch the data +recorded previously using the :js:class:`~openerp.web.Query` interface +and add this data to an ordered list added to the widget's template: + +.. patch:: + +And for consistency's sake (so that the display a user leaves is +pretty much the same as the one he comes back to), newly created +records should also automatically be added to the list: + +.. patch:: + +Note that we're only displaying the record once we know it's been +saved from the database (the ``create`` call has returned without +error). + +Mic check, is this working? +--------------------------- + +So far, features have been implemented, code has been worked and +tentatively tried. However, there is no guarantee they will *keep +working* as new changes are performed, new features added, … + +The original author (you, dear reader) could keep a notebook with a +list of workflows to check, to ensure everything keeps working. And +follow the notebook day after day, every time something is changed in +the module. + +That gets repetitive after a while. And computers are good at doing +repetitive stuff, as long as you tell them how to do it. + +So let's add test to the module, so that in the future the computer +can take care of ensuring what works today keeps working tomorrow. + +.. note:: + + Here we're writing tests after having implemented the widget. This + may or may not work, we may need to alter bits and pieces of code + to get them in a testable state. An other testing methodology is + :abbr:`TDD (Test-Driven Development)` where the tests are written + first, and the code necessary to make these tests pass is written + afterwards. + + Both methods have their opponents and detractors, advantages and + inconvenients. Pick the one you prefer. + +The first step of :doc:`testing` is to set up the basic testing +structure: + +1. Creating a javascript file + + .. patch:: + +2. Containing a test section (and a few tests to make sure the tests + are correctly run) + + .. patch:: + +3. Then declaring the test file in the module's manifest + + .. patch:: + +4. And finally — after restarting OpenERP — navigating to the test + runner at ``/web/tests`` and selecting your soon-to-be-tested + module: + + .. image:: module/testing_0.png + :align: center + + the testing result do indeed match the test. + +The simplest tests to write are for synchronous pure +functions. Synchronous means no RPC call or any other such thing +(e.g. ``setTimeout``), only direct data processing, and pure means no +side-effect: the function takes some input, manipulates it and yields +an output. + +In our widget, only ``format_time`` fits the bill: it takes a duration +(in milliseconds) and returns an ``hours:minutes:second`` formatting +of it. Let's test it: + +.. patch:: + +This series of simple tests passes with no issue. The next easy-ish +test type is to test basic DOM alterations from provided input, such +as (for our widget) updating the counter or displaying a record to the +records list: while it's not pure (it alters the DOM "in-place") it +has well-delimited side-effects and these side-effects come solely +from the provided input. + +Because these methods alter the widget's DOM, the widget needs a +DOM. Looking up :doc:`a widget's lifecycle <widget>`, the widget +really only gets its DOM when adding it to the document. However a +side-effect of this is to :js:func:`~openerp.web.Widget.start` it, +which for us means going to query the user's times. + +We don't have any records to get in our test, and we don't want to +test the initialization yet! So let's cheat a bit: we can manually +:js:func:`set a widget's DOM <openerp.web.Widget.setElement>`, let's +create a basic DOM matching what each method expects then call the +method: + +.. patch:: + +The next group of patches (in terms of setup/complexity) is RPC tests: +testing components/methods which perform network calls (RPC +requests). In our module, ``start`` and ``watch_stop`` are in that +case: ``start`` fetches the user's recorded times and ``watch_stop`` +creates a new record with the current watch. + +By default, tests don't allow RPC requests and will generate an error +when trying to perform one: + +.. image:: module/testing_1.png + :align: center + +To allow them, the test case (or the test suite) has to explicitly opt +into :js:attr:`rpc support <TestOptions.rpc>` by adding the ``rpc: +'mock'`` option to the test case, and providing its own "rpc +responses": + +.. patch:: + +.. note:: + + By default, tests cases don't load templates either. We had not + needed to perform any template rendering before here, so we must + now enable templates loading via :js:attr:`the corresponding + option <TestOptions.templates>`. + +Our final test requires altering the module's code: asynchronous tests +use :doc:`deferred </async>` to know when a test ends and the other +one can start (otherwise test content will execute non-linearly and +the assertions of a test will be executed during the next test or +worse), but although ``watch_stop`` performs an asynchronous +``create`` operation it doesn't return a deferred we can synchronize +on. We simply need to return its result: + +.. patch:: + +This makes no difference to the original code, but allows us to write +our test: + +.. patch:: + +.. [#DOM-building] they are not alternative solutions: they work very + well together. Templates are used to build "just + DOM", sub-widgets are used to build DOM subsections + *and* delegate part of the behavior (e.g. events + handling). + +.. _javascript module: + http://addyosmani.com/resources/essentialjsdesignpatterns/book/#modulepatternjavascript |
