From bdddf2269d4990df5df8d04cf4f3eca46c5de3e6 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Sun, 27 Mar 2011 14:05:15 +0200 Subject: [PATCH] [ADD] document documentation (esp. JS APIDoc via JsDoc, which is actually a fairly complex endeavour) bzr revid: xmo@openerp.com-20110327120515-wdcxc19j4exuu2zu --- addons/base/static/openerp/js/base.js | 5 + addons/base/static/openerp/js/base_chrome.js | 38 ++-- addons/base/static/openerp/js/base_views.js | 121 ++++++++-- doc/source/project.rst | 219 +++++++++++++++++++ 4 files changed, 347 insertions(+), 36 deletions(-) diff --git a/addons/base/static/openerp/js/base.js b/addons/base/static/openerp/js/base.js index 9206579cf73..8b7e704e6c3 100644 --- a/addons/base/static/openerp/js/base.js +++ b/addons/base/static/openerp/js/base.js @@ -65,6 +65,10 @@ // OpenERP initialisation and black magic about the pool //--------------------------------------------------------- +/** + * @name openerp + * @namespace + */ (function() { if (this.openerp) return; @@ -106,6 +110,7 @@ // OpenERP initialisation and black magic about the pool //--------------------------------------------------------- +/** @namespace */ openerp.base = function(instance) { openerp.base$chrome(instance); openerp.base$views(instance); diff --git a/addons/base/static/openerp/js/base_chrome.js b/addons/base/static/openerp/js/base_chrome.js index 1cf78618817..4db5aed6779 100644 --- a/addons/base/static/openerp/js/base_chrome.js +++ b/addons/base/static/openerp/js/base_chrome.js @@ -5,17 +5,6 @@ openerp.base$chrome = function(openerp) { openerp.base.callback = function(obj, method) { - // openerp.base.callback( obj, methods, [arg1, arg2, ... ] ) - // - // The callback object holds a chain that can be altered: - // callback.add( handler , [arg1, arg2, ... ] ) - // callback.add( { - // callback: function - // self: object or null - // args: array - // position: "first" or "last" - // unique: boolean - // }) var callback = function() { var args = Array.prototype.slice.call(arguments); var r; @@ -64,11 +53,13 @@ openerp.base.callback = function(obj, method) { }); }; -openerp.base.BasicController = Class.extend({ +openerp.base.BasicController = Class.extend( + /** @lends openerp.base.BasicController# */{ /** - * Controller contructor * rpc operations, event binding and callback calling should be done in * start() instead of init so that event can be hooked in between. + * + * @constructs */ init: function(element_id) { this.element_id = element_id; @@ -117,7 +108,15 @@ openerp.base.Console = openerp.base.BasicController.extend({ } }); -openerp.base.Session = openerp.base.BasicController.extend({ +openerp.base.Session = openerp.base.BasicController.extend( + /** @lends openerp.base.Session# */{ + /** + * @constructs + * @extends openerp.base.BasicController + * @param element_id + * @param server + * @param port + */ init: function(element_id, server, port) { this._super(element_id); this.server = (server == undefined) ? location.hostname : server; @@ -259,6 +258,7 @@ openerp.base.Session = openerp.base.BasicController.extend({ /** * Fetches a cookie stored by an openerp session * + * @private * @param name the cookie's name */ get_cookie: function (name) { @@ -273,8 +273,9 @@ openerp.base.Session = openerp.base.BasicController.extend({ return null; }, /** - * Create a new secure cookie with the provided name and value + * Create a new cookie with the provided name and value * + * @private * @param name the cookie's name * @param value the cookie's value * @param ttl the cookie's time to live, 1 year by default, set to -1 to delete @@ -328,7 +329,12 @@ openerp.base.Database = openerp.base.BasicController.extend({ // Non Session Controller to manage databases }); -openerp.base.Controller = openerp.base.BasicController.extend({ +openerp.base.Controller = openerp.base.BasicController.extend( + /** @lends openerp.base.Controller# */{ + /** + * @constructs + * @extends openerp.base.BasicController + */ init: function(session, element_id) { this._super(element_id); this.session = session; diff --git a/addons/base/static/openerp/js/base_views.js b/addons/base/static/openerp/js/base_views.js index c1ac7897302..a20242be5d8 100644 --- a/addons/base/static/openerp/js/base_views.js +++ b/addons/base/static/openerp/js/base_views.js @@ -115,12 +115,14 @@ openerp.base.ViewManager = openerp.base.Controller.extend({ }, on_edit: function() { }, - do_search: function (domains, contexts) { + do_search: function (domains, contexts, group_contexts) { var self = this; this.rpc('/base/session/eval_domain_and_context', { domains: domains, - contexts: contexts + contexts: contexts, + groupby: group_contexts }, function (results) { + // TODO: group by self.dataset.set({ context: results.context, domain: results.domain @@ -137,11 +139,19 @@ openerp.base.ViewManagerRoot = openerp.base.Controller.extend({ openerp.base.ViewManagerUsedAsAMany2One = openerp.base.Controller.extend({ }); -/** - * Management interface between views and the collection of selected OpenERP - * records (represents the view's state?) - */ -openerp.base.DataSet = openerp.base.Controller.extend({ +openerp.base.DataSet = openerp.base.Controller.extend( + /** @lends openerp.base.DataSet# */{ + + /** + * Management interface between views and the collection of selected OpenERP + * records (represents the view's state?) + * + * @constructs + * @extends openerp.base.Controller + * + * @param {openerp.base.Session} session current OpenERP session + * @param {String} model the OpenERP model this dataset will manage + */ init: function(session, model) { this._super(session); this.model = model; @@ -205,10 +215,9 @@ openerp.base.DataSet = openerp.base.Controller.extend({ return this; }, /** - * @event - * * Fires after the DataSet fetched the records matching its internal ids selection - * + * + * @event * @param {Array} records An array of the DataRecord fetched * @param event The on_fetch event object * @param {Number} event.offset the offset with which the original DataSet#fetch call was performed @@ -239,10 +248,9 @@ openerp.base.DataSet = openerp.base.Controller.extend({ return this; }, /** - * @event - * * Fires after the DataSet fetched the records matching its internal active ids selection * + * @event * @param {Array} records An array of the DataRecord fetched */ on_active_ids: function (records) { }, @@ -268,6 +276,7 @@ openerp.base.DataSet = openerp.base.Controller.extend({ /** * Fires after the DataSet fetched the record matching the current active record * + * @event * @param record the record matching the provided id, or null if there is no record for this id */ on_active_id: function (record) { @@ -391,7 +400,25 @@ openerp.base.DataRecord = openerp.base.Controller.extend({ } }); -openerp.base.SearchView = openerp.base.Controller.extend({ +openerp.base.SearchView = openerp.base.Controller.extend( + /** @lends openerp.base.SearchView# */{ + /** + * Manager for the Search view type. + * + * Handles laying out and rendering the various search widgets, as well + * as collecting contexts and domain and broadcasting them to whoever + * registered itself on the :js:func:`~openep.base.SearchView.on_search` + * event. + * + * @constructs + * @extends openerp.base.Controller + * + * @param {openerp.base.Session} session the current OpenERP session + * @param {String} element_id the root element where the search view should render itself + * @param {openerp.base.DataSet} dataset the dataset with which the search view will work + * @param {Number} view_id the id of this view object + * @param {Object} defaults default values to set on the various fields of the view + */ init: function(session, element_id, dataset, view_id, defaults) { this._super(session, element_id); this.dataset = dataset; @@ -539,7 +566,13 @@ openerp.base.SearchView = openerp.base.Controller.extend({ map(function (input) { return input.get_context(); }). compact(). value(); - this.on_search(domains, contexts); + // TODO: group_by on field contexts? + var group_contexts = _(this.enabled_filters). + chain(). + map(function (filter) { return filter.get_context(); }). + compact(). + value(); + this.on_search(domains, contexts, group_contexts); }, /** * Event hook for searches: triggers after the SearchView has collected @@ -551,8 +584,9 @@ openerp.base.SearchView = openerp.base.Controller.extend({ * * @param {Array} domains an array of string or literal domains * @param {Array} contexts an array of string or literal contexts + * @param {Array} group_contexts an ordered array of contexts which may need to be used to resolve grouping */ - on_search: function (domains, contexts) { }, + on_search: function (domains, contexts, group_contexts) { }, do_clear: function (e) { if (e && e.preventDefault) { e.preventDefault(); } this.on_clear(); @@ -583,8 +617,19 @@ openerp.base.SearchView = openerp.base.Controller.extend({ } }); +/** @namespace */ openerp.base.search = {}; -openerp.base.search.Invalid = Class.extend({ +openerp.base.search.Invalid = Class.extend( + /** @lends openerp.base.search.Invalid# */{ + /** + * Exception thrown by search widgets when they hold invalid values, + * which they can not return when asked. + * + * @constructs + * @param field the name of the field holding an invalid value + * @param value the invalid value + * @param message validation failure message + */ init: function (field, value, message) { this.field = field; this.value = value; @@ -595,8 +640,17 @@ openerp.base.search.Invalid = Class.extend({ ': [' + this.value + '] is ' + this.message); } }); -openerp.base.search.Widget = openerp.base.Controller.extend({ +openerp.base.search.Widget = openerp.base.Controller.extend( + /** @lends openerp.base.search.Widget# */{ template: null, + /** + * Root class of all search widgets + * + * @constructs + * @extends openerp.base.Controller + * + * @param view the ancestor view of this widget + */ init: function (view) { this.view = view; }, @@ -682,7 +736,14 @@ openerp.base.search.Group = openerp.base.search.Widget.extend({ }); } }); -openerp.base.search.Input = openerp.base.search.Widget.extend({ +openerp.base.search.Input = openerp.base.search.Widget.extend( + /** @lends openerp.base.search.Input# */{ + /** + * @constructs + * @extends openerp.base.search.Widget + * + * @param view + */ init: function (view) { this._super(view); this.view.inputs.push(this); @@ -747,12 +808,21 @@ openerp.base.search.Filter = openerp.base.search.Input.extend({ return this.attrs.domain; } }); -openerp.base.search.Field = openerp.base.search.Input.extend({ +openerp.base.search.Field = openerp.base.search.Input.extend( + /** @lends openerp.base.search.Field# */ { template: 'SearchView.field', default_operator: '=', // TODO: set default values // TODO: get context, domain // TODO: holds Filters + /** + * @constructs + * @extends openerp.base.search.Input + * + * @param view_section + * @param field + * @param view + */ init: function (view_section, field, view) { this._super(view); this.attrs = _.extend({}, field, view_section.attrs); @@ -795,7 +865,18 @@ openerp.base.search.Field = openerp.base.search.Input.extend({ return this.attrs['filter_domain']; } }); -openerp.base.search.CharField = openerp.base.search.Field.extend({ +/** + * Implementation of the ``char`` OpenERP field type: + * + * * Default operator is ``ilike`` rather than ``=`` + * * The Javascript and the HTML values are identical (strings) + * + * @class + * @extends openerp.base.search.Field + * + */ +openerp.base.search.CharField = openerp.base.search.Field.extend( + /** @lends openerp.base.search.CharField# */ { default_operator: 'ilike', get_value: function () { return this.$element.val(); diff --git a/doc/source/project.rst b/doc/source/project.rst index 78beae9dedb..5ef06a7280a 100644 --- a/doc/source/project.rst +++ b/doc/source/project.rst @@ -19,6 +19,193 @@ Merge proposals Writing documentation +++++++++++++++++++++ +The OpenERP Web project documentation uses Sphinx_ for the literate +documentation (this document for instance), the development guides +(for Python and Javascript alike) and the Python API documentation +(via autodoc_). + +For the Javascript API, documentation should be written using the +`JsDoc Toolkit`_. + +Guides and main documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The meat and most important part of all documentation. Should be +written in plain english, using reStructuredText_ and taking advantage +of `Sphinx's extensions`_, especially `cross-references`_. + +Python API Documentation +~~~~~~~~~~~~~~~~~~~~~~~~ + +All public objects in Python code should have a docstring written in +RST, using Sphinx's `Python domain`_ [#]_: + +* Functions and methods documentation should be in their own + docstring, using Sphinx's `info fields`_ + + For parameters types, built-in and stdlib types should be using the + combined syntax:: + + :param dict foo: what the purpose of foo is + + unless a more extensive explanation needs to be given (e.g. the + specification that the input should be a list of 3-tuple needs to + use ``:type:`` even though all types involved are built-ins). Any + other type should be specified in full with a cross-reference using + the ``:type:`` field:: + + :param foo: what the purpose of foo is + :type foo: :class:`some.addon.Class` + + Likewise, mentions of other methods (including within the same + class), modules or types should be cross-referenced. + +* Classes should likewise be documented using their own docstring, and + should include the documentation of their construction (``__init__`` + and ``__new__``), using the `info fields`_ as well. + +* Attributes (class and instance) should be documented in their + class's docstrin via the ``.. attribute::`` directiveg, following + the class's own documentation. + +* The relation between modules and module-level attributes is similar: + modules should be documented in their own docstring, public module + attributes should be documented in the module's docstring using the + ``.. data::`` directive. + +Javascript API documentation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Javascript API documentation uses JsDoc_, a javascript documentation +toolkit with a syntax similar to (and inspired by) JavaDoc's. + +Due to limitations of JsDoc, the coding patterns in OpenERP Web and +the Sphinx integration, there are a few peculiarities to be aware of +when writing javascript API documentation: + +* Namespaces and classes *must* be explicitly marked up even if they + are not documented, or JsDoc will not understand what they are and + will not generate documentation for their content. + + As a result, the bare minimum for a namespace is:: + + /** @namespace */ + foo.bar.baz = {}; + + while for a class it is:: + + /** @class */ + foo.bar.baz.Qux = [...] + +* Because the OpenERP Web project uses `John Resig's Class + implementation`_ instead of direct prototypal inheritance [#]_, + JsDoc fails to infer class scopes (and constructors or super + classes, for that matter) and has to be told explicitly. + + See :ref:`js-class-doc` for the complete rundown. + +* Much like the JavaDoc, JsDoc does not include a full markup + language. Instead, comments are simply marked up in HTML. + + This has a number of inconvenients: + + * Complex documentation comments become nigh-unreadable to read in + text editors (as opposed to IDEs, which may handle rendering + documentation comments on the fly) + + * Though cross-references are supported by JsDoc (via ``@link`` and + ``@see``), they only work within the JsDoc + + * More general impossibility to integrate correctly with Sphinx, and + e.g. reference JavaScript objects from a tutorial, or have all the + documentation live at the same place. + + As a result, JsDoc comments should be marked up using RST, not + HTML. They may use Sphinx's cross-references as well. + +.. _js-class-doc: + +Documenting a Class +******************* + +The first task when documenting a class using JsDoc is to *mark* that +class, so JsDoc knows it can be used to instantiate objects (and, more +importantly as far as it's concerned, should be documented with +methods and attributes and stuff). + +This is generally done through the ``@class`` tag, but this tag has a +significant limitation: it "believes" the constructor and the class +are one and the same [#]_. This will work for constructor-less +classes, but because OpenERP Web uses Resig's class the constructor is +not the class itself but its ``init()`` method. + +Because this pattern is common in modern javascript code bases, JsDoc +supports it: it is possible to mark an arbitrary instance method as +the *class specification* by using the ``@constructs`` tag. + +.. warning:: ``@constructs`` is a class specification in and of + itself, it *completely replaces* the class documentation. + + Using both a class documentation (even without ``@class`` itself) + and a constructor documentation is an *error* in JsDoc and will + result in incorrect behavior and broken documentation. + +The second issue is that Resig's class uses an object literal to +specify instance methods, and because JsDoc does not know anything +about Resig's class, it does not know about the role of the object +literal. + +As with constructors, though, JsDoc provides a pluggable way to tell +it about methods: the ``@lends`` tag. It specifies that the object +literal "lends" its properties to the class being built. + +``@lends`` must be specified right before the opening brace of the +object literal (between the opening paren of the ``#extend`` call and +the brace), and takes the full qualified name of the class being +created as a parameter, followed by the character ``#`` or by +``.prototype``. This latter part tells JsDoc these are instance +methods, not class (static) methods.. + +Finally, specifying a class's superclass is done through the +``@extends`` tag, which takes a fully qualified class name as a +parameter. + +Here are a class without a constructor, and a class with one, so that +everything is clear (these are straight from the OpenERP Web source, +with the descriptions and irrelevant atttributes stripped): + +.. code-block:: javascript + + /** + * + * + * @class + * @extends openerp.base.search.Field + */ + openerp.base.search.CharField = openerp.base.search.Field.extend( + /** @lends openerp.base.search.CharField# */ { + // methods here + }); + +.. code-block:: javascript + + + openerp.base.search.Widget = openerp.base.Controller.extend( + /** @lends openerp.base.search.Widget# */{ + /** + * + * + * @constructs + * @extends openerp.base.Controller + * + * @param view the ancestor view of this widget + */ + init: function (view) { + // construction of the instance + }, + // bunch of other methods + }); + OpenERP Web over time --------------------- @@ -30,3 +217,35 @@ Roadmap Release notes +++++++++++++ + +.. [#] because Python is the default domain, the ``py:`` markup prefix + is optional and should be left out. + +.. [#] Resig's Class still uses prototypes under the hood, it doesn't + reimplement its own object system although it does add several + helpers such as the ``_super()`` instance method. + +.. [#] Which is the case in normal Javascript semantics. Likewise, the + ``.prototype`` / ``#`` pattern we will see later on is due to + JsDoc defaulting to the only behavior it can rely on: "normal" + Javascript prototype-based type creation. + +.. _reStructuredText: + http://docutils.sourceforge.net/rst.html +.. _Sphinx: + http://sphinx.pocoo.org/index.html +.. _Sphinx's extensions: + http://sphinx.pocoo.org/markup/index.html +.. _Python domain: + http://sphinx.pocoo.org/domains.html#the-python-domain +.. _info fields: + http://sphinx.pocoo.org/domains.html#info-field-lists +.. _autodoc: + http://sphinx.pocoo.org/ext/autodoc.html?highlight=autodoc#sphinx.ext.autodoc +.. _cross-references: + http://sphinx.pocoo.org/markup/inline.html#xref-syntax +.. _JsDoc: +.. _JsDoc Toolkit: + http://code.google.com/p/jsdoc-toolkit/ +.. _John Resig's Class implementation: + http://ejohn.org/blog/simple-javascript-inheritance/