summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Morel <xmo@openerp.com>2011-03-27 14:05:15 +0200
committerXavier Morel <xmo@openerp.com>2011-03-27 14:05:15 +0200
commitbdddf2269d4990df5df8d04cf4f3eca46c5de3e6 (patch)
tree7e9951223275a8067c1ddb0666b5ec781ace3e03
parentab43c4ee485e5071dd32bcd7d16cc70a1fa03998 (diff)
[ADD] document documentation (esp. JS APIDoc via JsDoc, which is actually a fairly complex endeavour)
bzr revid: xmo@openerp.com-20110327120515-wdcxc19j4exuu2zu
-rw-r--r--addons/base/static/openerp/js/base.js5
-rw-r--r--addons/base/static/openerp/js/base_chrome.js38
-rw-r--r--addons/base/static/openerp/js/base_views.js121
-rw-r--r--doc/source/project.rst219
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
+
+ /**
+ * <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/