[ADD] document documentation (esp. JS APIDoc via JsDoc, which is actually a fairly complex endeavour)

bzr revid: xmo@openerp.com-20110327120515-wdcxc19j4exuu2zu
This commit is contained in:
Xavier Morel 2011-03-27 14:05:15 +02:00
parent ab43c4ee48
commit bdddf2269d
4 changed files with 347 additions and 36 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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();

View File

@ -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
/**
* <Insert description here, not below>
*
* @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# */{
/**
* <Insert description here, not below>
*
* @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/