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 | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/doc')
62 files changed, 4237 insertions, 0 deletions
diff --git a/addons/web/doc/Makefile b/addons/web/doc/Makefile new file mode 100644 index 00000000..c1eff186 --- /dev/null +++ b/addons/web/doc/Makefile @@ -0,0 +1,154 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = -q +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext + +help: + @echo "Please use \`make <target>' where <target> is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + sed -i '/-99999/d' _build/dirhtml/_static/flasky.css + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OpenERPTechnicalDocumentation.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OpenERPTechnicalDocumentation.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/OpenERPTechnicalDocumentation" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OpenERPTechnicalDocumentation" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/addons/web/doc/_static/openerp.png b/addons/web/doc/_static/openerp.png Binary files differnew file mode 100644 index 00000000..caa1b21d --- /dev/null +++ b/addons/web/doc/_static/openerp.png diff --git a/addons/web/doc/_templates/sidebarintro.html b/addons/web/doc/_templates/sidebarintro.html new file mode 100644 index 00000000..bbe4c472 --- /dev/null +++ b/addons/web/doc/_templates/sidebarintro.html @@ -0,0 +1,16 @@ +<p class="logo"><a href="https://doc.odoo.com/"> + <img class="logo" src="{{ pathto('_static/openerp.png', 1) }}" alt="Logo"/> +</a></p> + +<h3>Other Docs</h3> +<ul> + <li><a href="https://doc.odoo.com/trunk">Odoo Developers Documentation</a></li> + <li><a href="https://doc.odoo.com/trunk/server">Odoo Server Developers Documentation</a></li> + <li><a href="https://doc.odoo.com/book">Odoo Users Documentation</a></li> +</ul> + +<h3>Useful Links</h3> +<ul> + <li><a href="https://www.odoo.com/">The Odoo website</a></li> + <li><a href="http://python.org/">The Python programming language</a></li> +</ul> diff --git a/addons/web/doc/_templates/sidebarlogo.html b/addons/web/doc/_templates/sidebarlogo.html new file mode 100644 index 00000000..de6e3e5c --- /dev/null +++ b/addons/web/doc/_templates/sidebarlogo.html @@ -0,0 +1,3 @@ +<p class="logo"><a href="{{ pathto(master_doc) }}"> + <img class="logo" src="{{ pathto('_static/openerp.png', 1) }}" alt="Logo"/> +</a></p> diff --git a/addons/web/doc/_themes/LICENSE b/addons/web/doc/_themes/LICENSE new file mode 100644 index 00000000..8daab7ee --- /dev/null +++ b/addons/web/doc/_themes/LICENSE @@ -0,0 +1,37 @@ +Copyright (c) 2010 by Armin Ronacher. + +Some rights reserved. + +Redistribution and use in source and binary forms of the theme, with or +without modification, are permitted provided that the following conditions +are met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* The names of the contributors may not be used to endorse or + promote products derived from this software without specific + prior written permission. + +We kindly ask you to only use these themes in an unmodified manner just +for Flask and Flask-related products, not for unrelated projects. If you +like the visual style and want to use it for your own projects, please +consider making some larger changes to the themes (such as changing +font faces, sizes, colors or margins). + +THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/addons/web/doc/_themes/README b/addons/web/doc/_themes/README new file mode 100644 index 00000000..b3292bdf --- /dev/null +++ b/addons/web/doc/_themes/README @@ -0,0 +1,31 @@ +Flask Sphinx Styles +=================== + +This repository contains sphinx styles for Flask and Flask related +projects. To use this style in your Sphinx documentation, follow +this guide: + +1. put this folder as _themes into your docs folder. Alternatively + you can also use git submodules to check out the contents there. +2. add this to your conf.py: + + sys.path.append(os.path.abspath('_themes')) + html_theme_path = ['_themes'] + html_theme = 'flask' + +The following themes exist: + +- 'flask' - the standard flask documentation theme for large + projects +- 'flask_small' - small one-page theme. Intended to be used by + very small addon libraries for flask. + +The following options exist for the flask_small theme: + + [options] + index_logo = '' filename of a picture in _static + to be used as replacement for the + h1 in the index.rst file. + index_logo_height = 120px height of the index logo + github_fork = '' repository name on github for the + "fork me" badge diff --git a/addons/web/doc/_themes/flask/layout.html b/addons/web/doc/_themes/flask/layout.html new file mode 100644 index 00000000..ad08eccd --- /dev/null +++ b/addons/web/doc/_themes/flask/layout.html @@ -0,0 +1,25 @@ +{%- extends "basic/layout.html" %} +{%- block extrahead %} + {{ super() }} + {% if theme_touch_icon %} + <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" /> + {% endif %} + <link media="only screen and (max-device-width: 480px)" href="{{ + pathto('_static/small_flask.css', 1) }}" type= "text/css" rel="stylesheet" /> +{% endblock %} +{%- block relbar2 %}{% endblock %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} + <div class=indexwrapper> + {% endif %} +{% endblock %} +{%- block footer %} + <div class="footer"> + © Copyright {{ copyright }} + Created using <a href="http://sphinx.pocoo.org/">Sphinx</a> and a modified <a href="https://github.com/mitsuhiko/flask-sphinx-themes">Flask theme</a>. + </div> + {% if pagename == 'index' %} + </div> + {% endif %} +{%- endblock %} diff --git a/addons/web/doc/_themes/flask/relations.html b/addons/web/doc/_themes/flask/relations.html new file mode 100644 index 00000000..3bbcde85 --- /dev/null +++ b/addons/web/doc/_themes/flask/relations.html @@ -0,0 +1,19 @@ +<h3>Related Topics</h3> +<ul> + <li><a href="{{ pathto(master_doc) }}">Documentation overview</a><ul> + {%- for parent in parents %} + <li><a href="{{ parent.link|e }}">{{ parent.title }}</a><ul> + {%- endfor %} + {%- if prev %} + <li>Previous: <a href="{{ prev.link|e }}" title="{{ _('previous chapter') + }}">{{ prev.title }}</a></li> + {%- endif %} + {%- if next %} + <li>Next: <a href="{{ next.link|e }}" title="{{ _('next chapter') + }}">{{ next.title }}</a></li> + {%- endif %} + {%- for parent in parents %} + </ul></li> + {%- endfor %} + </ul></li> +</ul> diff --git a/addons/web/doc/_themes/flask/static/flasky.css_t b/addons/web/doc/_themes/flask/static/flasky.css_t new file mode 100644 index 00000000..9fec3190 --- /dev/null +++ b/addons/web/doc/_themes/flask/static/flasky.css_t @@ -0,0 +1,396 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * :copyright: Copyright 2010 by Armin Ronacher. + * :license: Flask Design License, see LICENSE for details. + */ + +{% set page_width = '80em' %} +{% set sidebar_width = '16em' %} + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Georgia', serif; + font-size: 15px; + background-color: white; + color: #000; + margin: 0; + padding: 0; +} + +div.document { + width: {{ page_width }}; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 {{ sidebar_width }}; +} + +div.sphinxsidebar { + width: {{ sidebar_width }}; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 0px 0 0px; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + width: {{ page_width }}; + margin: 20px auto 30px auto; + font-size: 12px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +div.related { + display: none; +} + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebar { + font-size: 12px; + line-height: 1.5; +} + +div.sphinxsidebarwrapper { + padding: 0px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0 0 20px 0; + margin: 0; + text-align: center; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: 'Garamond', 'Georgia', serif; + color: #444; + font-size: 22px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 18px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar input { + border: 1px solid #ccc; + font-family: 'Georgia', serif; + font-size: 1em; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +{% if theme_index_logo %} +div.indexwrapper h1 { + /* We don't want it, I don't know why theme_index_logo is triggered. */ + /* text-indent: -999999px; */ + background: url({{ theme_index_logo }}) no-repeat center center; + height: {{ theme_index_logo_height }}; +} +{% endif %} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #ddd; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition tt.xref, div.admonition a tt { + border-bottom: 1px solid #fafafa; +} + +dd div.admonition { + margin-left: -60px; + padding-left: 60px; +} + +div.admonition p.admonition-title { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + font-size: 22px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; + background: #fdfdfd; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td.label { + width: 0px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #eee; + padding: 7px 30px; + margin: 15px -30px; + line-height: 1.3em; +} + +dl pre, blockquote pre, li pre { + margin-left: -60px; + padding-left: 60px; +} + +dl dl pre { + margin-left: -90px; + padding-left: 90px; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid white; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt { + background: #EEE; +} diff --git a/addons/web/doc/_themes/flask/static/small_flask.css b/addons/web/doc/_themes/flask/static/small_flask.css new file mode 100644 index 00000000..1c6df309 --- /dev/null +++ b/addons/web/doc/_themes/flask/static/small_flask.css @@ -0,0 +1,70 @@ +/* + * small_flask.css_t + * ~~~~~~~~~~~~~~~~~ + * + * :copyright: Copyright 2010 by Armin Ronacher. + * :license: Flask Design License, see LICENSE for details. + */ + +body { + margin: 0; + padding: 20px 30px; +} + +div.documentwrapper { + float: none; + background: white; +} + +div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: white; +} + +div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, +div.sphinxsidebar h3 a { + color: white; +} + +div.sphinxsidebar a { + color: #aaa; +} + +div.sphinxsidebar p.logo { + display: none; +} + +div.document { + width: 100%; + margin: 0; +} + +div.related { + display: block; + margin: 0; + padding: 10px 0 20px 0; +} + +div.related ul, +div.related ul li { + margin: 0; + padding: 0; +} + +div.footer { + display: none; +} + +div.bodywrapper { + margin: 0; +} + +div.body { + min-height: 0; + padding: 0; +} diff --git a/addons/web/doc/_themes/flask/theme.conf b/addons/web/doc/_themes/flask/theme.conf new file mode 100644 index 00000000..18c720f8 --- /dev/null +++ b/addons/web/doc/_themes/flask/theme.conf @@ -0,0 +1,9 @@ +[theme] +inherit = basic +stylesheet = flasky.css +pygments_style = flask_theme_support.FlaskyStyle + +[options] +index_logo = '' +index_logo_height = 120px +touch_icon = diff --git a/addons/web/doc/_themes/flask_small/layout.html b/addons/web/doc/_themes/flask_small/layout.html new file mode 100644 index 00000000..afd7a912 --- /dev/null +++ b/addons/web/doc/_themes/flask_small/layout.html @@ -0,0 +1,22 @@ +{% extends "basic/layout.html" %} +{% block header %} + {{ super() }} + {% if pagename == 'index' %} + <div class=indexwrapper> + {% endif %} +{% endblock %} +{% block footer %} + {% if pagename == 'index' %} + </div> + {% endif %} +{% endblock %} +{# do not display relbars #} +{% block relbar1 %}{% endblock %} +{% block relbar2 %} + {% if theme_github_fork %} + <a target="_blank" href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;" + src="static/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a> + {% endif %} +{% endblock %} +{% block sidebar1 %}{% endblock %} +{% block sidebar2 %}{% endblock %} diff --git a/addons/web/doc/_themes/flask_small/static/flasky.css_t b/addons/web/doc/_themes/flask_small/static/flasky.css_t new file mode 100644 index 00000000..fe2141c5 --- /dev/null +++ b/addons/web/doc/_themes/flask_small/static/flasky.css_t @@ -0,0 +1,287 @@ +/* + * flasky.css_t + * ~~~~~~~~~~~~ + * + * Sphinx stylesheet -- flasky theme based on nature theme. + * + * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: 'Georgia', serif; + font-size: 17px; + color: #000; + background: white; + margin: 0; + padding: 0; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 40px auto 0 auto; + width: 700px; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #ffffff; + color: #3E4349; + padding: 0 30px 30px 30px; +} + +img.floatingflask { + padding: 0 0 10px 10px; + float: right; +} + +div.footer { + text-align: right; + color: #888; + padding: 10px; + font-size: 14px; + width: 650px; + margin: 0 auto 40px auto; +} + +div.footer a { + color: #888; + text-decoration: underline; +} + +div.related { + line-height: 32px; + color: #888; +} + +div.related ul { + padding: 0 0 0 10px; +} + +div.related a { + color: #444; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body { + padding-bottom: 40px; /* saved for footer */ +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +{% if theme_index_logo %} +div.indexwrapper h1 { + text-indent: -999999px; + background: url({{ theme_index_logo }}) no-repeat center center; + height: {{ theme_index_logo_height }}; +} +{% endif %} + +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: white; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #eaeaea; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + background: #fafafa; + margin: 20px -30px; + padding: 10px 30px; + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; +} + +div.admonition p.admonition-title { + font-family: 'Garamond', 'Georgia', serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight{ + background-color: white; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.note { + background-color: #eee; + border: 1px solid #ccc; +} + +div.seealso { + background-color: #ffc; + border: 1px solid #ff6; +} + +div.topic { + background-color: #eee; +} + +div.warning { + background-color: #ffe4e4; + border: 1px solid #f66; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt { + font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.85em; +} + +img.screenshot { +} + +tt.descname, tt.descclassname { + font-size: 0.95em; +} + +tt.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #eee; + -webkit-box-shadow: 2px 2px 4px #eee; + box-shadow: 2px 2px 4px #eee; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #eee; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.footnote td { + padding: 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +pre { + padding: 0; + margin: 15px -30px; + padding: 8px; + line-height: 1.3em; + padding: 7px 30px; + background: #eee; + border-radius: 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; +} + +dl pre { + margin-left: -60px; + padding-left: 60px; +} + +tt { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, a tt { + background-color: #FBFBFB; +} + +a:hover tt { + background: #EEE; +} diff --git a/addons/web/doc/_themes/flask_small/static/forkme_right_darkblue_121621.png b/addons/web/doc/_themes/flask_small/static/forkme_right_darkblue_121621.png Binary files differnew file mode 100644 index 00000000..146ef8a8 --- /dev/null +++ b/addons/web/doc/_themes/flask_small/static/forkme_right_darkblue_121621.png diff --git a/addons/web/doc/_themes/flask_small/theme.conf b/addons/web/doc/_themes/flask_small/theme.conf new file mode 100644 index 00000000..542b4625 --- /dev/null +++ b/addons/web/doc/_themes/flask_small/theme.conf @@ -0,0 +1,10 @@ +[theme] +inherit = basic +stylesheet = flasky.css +nosidebar = true +pygments_style = flask_theme_support.FlaskyStyle + +[options] +index_logo = '' +index_logo_height = 120px +github_fork = '' diff --git a/addons/web/doc/_themes/flask_theme_support.py b/addons/web/doc/_themes/flask_theme_support.py new file mode 100644 index 00000000..33f47449 --- /dev/null +++ b/addons/web/doc/_themes/flask_theme_support.py @@ -0,0 +1,86 @@ +# flasky extensions. flasky pygments style based on tango style +from pygments.style import Style +from pygments.token import Keyword, Name, Comment, String, Error, \ + Number, Operator, Generic, Whitespace, Punctuation, Other, Literal + + +class FlaskyStyle(Style): + background_color = "#f8f8f8" + default_style = "" + + styles = { + # No corresponding class for the following: + #Text: "", # class: '' + Whitespace: "underline #f8f8f8", # class: 'w' + Error: "#a40000 border:#ef2929", # class: 'err' + Other: "#000000", # class 'x' + + Comment: "italic #8f5902", # class: 'c' + Comment.Preproc: "noitalic", # class: 'cp' + + Keyword: "bold #004461", # class: 'k' + Keyword.Constant: "bold #004461", # class: 'kc' + Keyword.Declaration: "bold #004461", # class: 'kd' + Keyword.Namespace: "bold #004461", # class: 'kn' + Keyword.Pseudo: "bold #004461", # class: 'kp' + Keyword.Reserved: "bold #004461", # class: 'kr' + Keyword.Type: "bold #004461", # class: 'kt' + + Operator: "#582800", # class: 'o' + Operator.Word: "bold #004461", # class: 'ow' - like keywords + + Punctuation: "bold #000000", # class: 'p' + + # because special names such as Name.Class, Name.Function, etc. + # are not recognized as such later in the parsing, we choose them + # to look the same as ordinary variables. + Name: "#000000", # class: 'n' + Name.Attribute: "#c4a000", # class: 'na' - to be revised + Name.Builtin: "#004461", # class: 'nb' + Name.Builtin.Pseudo: "#3465a4", # class: 'bp' + Name.Class: "#000000", # class: 'nc' - to be revised + Name.Constant: "#000000", # class: 'no' - to be revised + Name.Decorator: "#888", # class: 'nd' - to be revised + Name.Entity: "#ce5c00", # class: 'ni' + Name.Exception: "bold #cc0000", # class: 'ne' + Name.Function: "#000000", # class: 'nf' + Name.Property: "#000000", # class: 'py' + Name.Label: "#f57900", # class: 'nl' + Name.Namespace: "#000000", # class: 'nn' - to be revised + Name.Other: "#000000", # class: 'nx' + Name.Tag: "bold #004461", # class: 'nt' - like a keyword + Name.Variable: "#000000", # class: 'nv' - to be revised + Name.Variable.Class: "#000000", # class: 'vc' - to be revised + Name.Variable.Global: "#000000", # class: 'vg' - to be revised + Name.Variable.Instance: "#000000", # class: 'vi' - to be revised + + Number: "#990000", # class: 'm' + + Literal: "#000000", # class: 'l' + Literal.Date: "#000000", # class: 'ld' + + String: "#4e9a06", # class: 's' + String.Backtick: "#4e9a06", # class: 'sb' + String.Char: "#4e9a06", # class: 'sc' + String.Doc: "italic #8f5902", # class: 'sd' - like a comment + String.Double: "#4e9a06", # class: 's2' + String.Escape: "#4e9a06", # class: 'se' + String.Heredoc: "#4e9a06", # class: 'sh' + String.Interpol: "#4e9a06", # class: 'si' + String.Other: "#4e9a06", # class: 'sx' + String.Regex: "#4e9a06", # class: 'sr' + String.Single: "#4e9a06", # class: 's1' + String.Symbol: "#4e9a06", # class: 'ss' + + Generic: "#000000", # class: 'g' + Generic.Deleted: "#a40000", # class: 'gd' + Generic.Emph: "italic #000000", # class: 'ge' + Generic.Error: "#ef2929", # class: 'gr' + Generic.Heading: "bold #000080", # class: 'gh' + Generic.Inserted: "#00A000", # class: 'gi' + Generic.Output: "#888", # class: 'go' + Generic.Prompt: "#745334", # class: 'gp' + Generic.Strong: "bold #000000", # class: 'gs' + Generic.Subheading: "bold #800080", # class: 'gu' + Generic.Traceback: "bold #a40000", # class: 'gt' + } diff --git a/addons/web/doc/addon-structure.txt b/addons/web/doc/addon-structure.txt new file mode 100644 index 00000000..19b38689 --- /dev/null +++ b/addons/web/doc/addon-structure.txt @@ -0,0 +1,12 @@ +<addon name> + +-- __manifest__.py + +-- controllers/ + +-- static/ + +-- lib/ + +-- src/ + +-- css/ + +-- img/ + +-- js/ + +-- xml/ + +-- test/ + +-- test/ diff --git a/addons/web/doc/client_action.rst b/addons/web/doc/client_action.rst new file mode 100644 index 00000000..4852400d --- /dev/null +++ b/addons/web/doc/client_action.rst @@ -0,0 +1,114 @@ +.. highlight:: javascript + +Client actions +============== + +Client actions are the client-side version of OpenERP's "Server +Actions": instead of allowing for semi-arbitrary code to be executed +in the server, they allow for execution of client-customized code. + +On the server side, a client action is an action of type +``ir.actions.client``, which has (at most) two properties: a mandatory +``tag``, which is an arbitrary string by which the client will +identify the action, and an optional ``params`` which is simply a map +of keys and values sent to the client as-is (this way, client actions +can be made generic and reused in multiple contexts). + +General Structure +----------------- + +In the OpenERP Web code, a client action only requires two pieces of +information: + +* Mapping the action's ``tag`` to an object + +* Providing said object. Two different types of objects can be mapped + to a client action: + + * An OpenERP Web widget, which must inherit from + :js:class:`openerp.web.Widget` + + * A regular javascript function + +The major difference is in the lifecycle of these: + +* if the client action maps to a function, the function will be called + when executing the action. The function can have no further + interaction with the Web Client itself, although it can return an + action which will be executed after it. + + The function takes 2 parameters: the ActionManager calling it and + the descriptor for the current action (the ``ir.actions.client`` + dictionary). + +* if, on the other hand, the client action maps to a + :js:class:`~openerp.web.Widget`, that + :js:class:`~openerp.web.Widget` will be instantiated and added to + the web client's canvas, with the usual + :js:class:`~openerp.web.Widget` lifecycle (essentially, it will + either take over the content area of the client or it will be + integrated within a dialog). + +For example, to create a client action displaying a ``res.widget`` +object:: + + // Registers the object 'openerp.web_dashboard.Widget' to the client + // action tag 'board.home.widgets' + instance.web.client_actions.add( + 'board.home.widgets', 'instance.web_dashboard.Widget'); + instance.web_dashboard.Widget = instance.web.Widget.extend({ + template: 'HomeWidget' + }); + +At this point, the generic :js:class:`~openerp.web.Widget` lifecycle +takes over, the template is rendered, inserted in the client DOM, +bound on the object's ``$el`` property and the object is started. + +The second parameter to the constructor is the descriptor for the +action itself, which contains any parameter provided:: + + init: function (parent, action) { + // execute the Widget's init + this._super(parent); + // board.home.widgets only takes a single param, the identifier of the + // res.widget object it should display. Store it for later + this.widget_id = action.params.widget_id; + } + +More complex initialization (DOM manipulations, RPC requests, ...) +should be performed in the :js:func:`~openerp.web.Widget.start()` +method. + +.. note:: + + As required by :js:class:`~openerp.web.Widget`'s contract, if + :js:func:`~openerp.web.Widget.start()` executes any asynchronous + code it should return a ``Promise`` so callers know when it's + ready for interaction. + +.. code-block:: javascript + + start: function () { + return Promise.all([ + this._super(), + // Simply read the res.widget object this action should display + new instance.web.Model('res.widget').call( + 'read', [[this.widget_id], ['title']]) + .then(this.proxy('on_widget_loaded') + ]); + } + +The client action can then behave exactly as it wishes to within its +root (``this.$el``). In this case, it performs further renderings once +its widget's content is retrieved:: + + on_widget_loaded: function (widgets) { + var widget = widgets[0]; + var url = _.sprintf( + '/web_dashboard/widgets/content?session_id=%s&widget_id=%d', + this.session.session_id, widget.id); + this.$el.html(QWeb.render('HomeWidget.content', { + widget: widget, + url: url + })); + } diff --git a/addons/web/doc/conf.py b/addons/web/doc/conf.py new file mode 100644 index 00000000..111018ad --- /dev/null +++ b/addons/web/doc/conf.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +# +# OpenERP Technical Documentation configuration file, created by +# sphinx-quickstart on Fri Feb 17 16:14:06 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.append(os.path.abspath('_themes')) +sys.path.insert(0, os.path.abspath('../addons')) +sys.path.insert(0, os.path.abspath('..')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', 'sphinx.ext.intersphinx', + 'sphinx.ext.todo', 'sphinx.ext.viewcode', + 'patchqueue' +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'OpenERP Web Developers Documentation' +copyright = u'2012, OpenERP s.a.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '7.0' +# The full version, including alpha/beta/rc tags. +release = '7.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'flask' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = ['_themes'] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +html_sidebars = { + 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], + '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', + 'sourcelink.html', 'searchbox.html'] +} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'openerp-web-doc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'openerp-web-doc.tex', u'OpenERP Web Developers Documentation', + u'OpenERP s.a.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'openerp-web-doc', u'OpenERP Web Developers Documentation', + [u'OpenERP s.a.'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'OpenERPWebDocumentation', u'OpenERP Web Developers Documentation', + u'OpenERP s.a.', 'OpenERPWebDocumentation', 'Developers documentation for the openerp-web project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' + +todo_include_todos = True + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = { + 'python': ('http://docs.python.org/', None), + 'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None), +} diff --git a/addons/web/doc/form_view.rst b/addons/web/doc/form_view.rst new file mode 100644 index 00000000..98d4a4de --- /dev/null +++ b/addons/web/doc/form_view.rst @@ -0,0 +1,55 @@ +Notes on the usage of the Form View as a sub-widget +=================================================== + +Undocumented stuff +------------------ + +* ``initial_mode`` *option* defines the starting mode of the form + view, one of ``view`` and ``edit`` (?). Default value is ``view`` + (non-editable form). + +* ``embedded_view`` *attribute* has to be set separately when + providing a view directly, no option available for that usage. + + * View arch **must** contain node with + ``@class="oe_form_container"``, otherwise everything will break + without any info + + * Root element of view arch not being ``form`` may or may not work + correctly, no idea. + + * Freeform views => ``@version="7.0"`` + +* Form is not entirely loaded (some widgets may not appear) unless + ``on_record_loaded`` is called (or ``do_show``, which itself calls + ``on_record_loaded``). + +* "Empty" form => ``on_button_new`` (...), or manually call + ``default_get`` + ``on_record_loaded`` + +* Form fields default to width: 100%, padding, !important margin, can + be reached via ``.oe_form_field`` + +* Form *will* render buttons and a pager, offers options to locate + both outside of form itself (``$buttons`` and ``$pager``), providing + empty jquery objects (``$()``) seems to stop displaying both but not + sure if there are deleterious side-effects. + + Other options: + + * Pass in ``$(document.createDocumentFragment)`` to ensure it's a + DOM-compatible tree completely outside of the actual DOM. + + * ??? + +* readonly fields probably don't have a background, beware if need of + overlay + + * What is the difference between ``readonly`` and + ``effective_readonly``? + +* No facilities for DOM events handling/delegations e.g. handling + keyup/keydown/keypress from a form fields into the form's user. + + * Also no way to reverse from a DOM node (e.g. DOMEvent#target) back to a + form view field easily diff --git a/addons/web/doc/guidelines.rst b/addons/web/doc/guidelines.rst new file mode 100644 index 00000000..8721d372 --- /dev/null +++ b/addons/web/doc/guidelines.rst @@ -0,0 +1,194 @@ +Guidelines and Recommendations +============================== + +Web Module Recommendations +-------------------------- + +Identifiers (``id`` attribute) should be avoided +'''''''''''''''''''''''''''''''''''''''''''''''' + +In generic applications and modules, ``@id`` limits the reusabily of +components and tends to make code more brittle. + +Just about all the time, they can be replaced with nothing, with +classes or with keeping a reference to a DOM node or a jQuery element +around. + +.. note:: + + If it is *absolutely necessary* to have an ``@id`` (because a + third-party library requires one and can't take a DOM element), it + should be generated with `_.uniqueId + <http://underscorejs.org/#uniqueId>`_ or some other similar + method. + +Avoid predictable/common CSS class names +'''''''''''''''''''''''''''''''''''''''' + +Class names such as "content" or "navigation" might match the desired +meaning/semantics, but it is likely an other developer will have the +same need, creating a naming conflict and unintended behavior. Generic +class names should be prefixed with e.g. the name of the component +they belong to (creating "informal" namespaces, much as in C or +Objective-C) + +Global selectors should be avoided +'''''''''''''''''''''''''''''''''' + +Because a component may be used several times in a single page (an +example in OpenERP is dashboards), queries should be restricted to a +given component's scope. Unfiltered selections such as ``$(selector)`` +or ``document.querySelectorAll(selector)`` will generally lead to +unintended or incorrect behavior. + +OpenERP Web's :js:class:`~openerp.web.Widget` has an attribute +providing its DOM root :js:attr:`Widget.$el <openerp.web.Widget.$el>`, +and a shortcut to select nodes directly :js:attr:`Widget.$ +<openerp.web.Widget.$>`. + +More generally, never assume your components own or controls anything +beyond its own personal DOM. + +Understand deferreds +'''''''''''''''''''' + +Deferreds, promises, futures, … + +Known under many names, these objects are essential to and (in OpenERP +Web) widely used for making :doc:`asynchronous javascript operations +<async>` palatable and understandable. + +OpenERP Web guidelines +---------------------- + +* HTML templating/rendering should use :doc:`qweb` unless absolutely + trivial. + +* All interactive components (components displaying information to the + screen or intercepting DOM events) must inherit from + :class:`~openerp.web.Widget` and correctly implement and use its API + and lifecycle. + +* All css classes must be prefixed with *oe_* . + +* Asynchronous functions (functions which call :ref:`session.rpc + <rpc_rpc>` directly or indirectly at the very least) *must* return + deferreds, so that callers of overriders can correctly synchronize + with them. + +New Javascript guidelines +------------------------- + +From v11, we introduce a new coding standard for Odoo Javascript code. Here it +is: + +* add "use strict"; on top of every odoo JS module + +* name all entities exported by a JS module. So, instead of + + .. code-block:: javascript + + return Widget.extend({ + ... + }); + +you should use: + + .. code-block:: javascript + + var MyWidget = Widget.extend({ + ... + }); + + return MyWidget + +* there should be one space between function and the left parenthesis: + + .. code-block:: javascript + + function (a, b) {} + +* JS files should have a (soft) limit of 80 chars width, and a hard limit of 100 + +* document every functions and every files, with the style JSDoc. + +* for function overriding other functions, consider adding the tag @override in + the JS Doc. Also, you can mention which method is overridden: + + .. code-block:: javascript + + /** + * When a save operation has been confirmed from the model, this method is + * called. + * + * @override method from field manager mixin + * @param {string} id + * @returns {Deferred} + */ + _confirmSave: function (id) { + +* there should be an empty line between the main function comments and the tags, + or parameter descriptions + +* avoid introspection: don't build dynamically a method name and call it. It is + more fragile and more difficult to refactor + +* methods should be private if possible + +* never read an attribute of an attribute on somethig that you have a reference. + So, this is not good: + + .. code-block:: javascript + + this.myObject.propA.propB + +* never use a reference to the parent widget + +* avoid using the 'include' functionality: extending a class is fine and does + not cause issue, including a class is much more fragile, and may not work. + +* For the widgets, here is how the various attributes/functions should be + ordered: + + 1. all static attributes, such as template, events, custom_events, ... + + 2. all methods from the lifecycle of a widget, in this order: init, willStart, + start, destroy + + 3. If there are public methods, a section titled "Public", with an empty line + before and after + + 4. all public methods, camelcased, in alphabetic order + + 5. If there are private methods, a section titled "Private", with an empty line + before and after + + 6. all private methods, camelcased and prefixed with _, in alphabetic order + + 7. If there are event handlers, a section titled "Handlers", with an empty line + before and after + + 8. all handlers, camelcased and prefixed with _on, in alphabetic order + + 9. If there are static methods, they should be in a section titled "Static". + All static methods are considered public, camelcased with no _. + +* write unit tests + +* for the event handlers defined by the key 'event' or 'custom_events', don't + inline the function. Always add a string name, and add the definition in the + handler section + +* one space after if and for + +* never call private methods on another object + +* object definition on more than one line: each element should have a trailing + comma. + +* strings: double quotes for all textual strings (such as "Hello"), and single + quotes for all other strings, such as a css selector '.o_form_view' + +* always use this._super.apply(this, arguments); + +* keys in an object: ordered by alphabetic order
\ No newline at end of file diff --git a/addons/web/doc/images/db-query.png b/addons/web/doc/images/db-query.png Binary files differnew file mode 100644 index 00000000..e063b724 --- /dev/null +++ b/addons/web/doc/images/db-query.png diff --git a/addons/web/doc/images/runner.png b/addons/web/doc/images/runner.png Binary files differnew file mode 100644 index 00000000..bd48e9d2 --- /dev/null +++ b/addons/web/doc/images/runner.png diff --git a/addons/web/doc/images/runner2.png b/addons/web/doc/images/runner2.png Binary files differnew file mode 100644 index 00000000..38ea2949 --- /dev/null +++ b/addons/web/doc/images/runner2.png diff --git a/addons/web/doc/images/tests.png b/addons/web/doc/images/tests.png Binary files differnew file mode 100644 index 00000000..84083d9e --- /dev/null +++ b/addons/web/doc/images/tests.png diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png Binary files differnew file mode 100644 index 00000000..c8a6f8ae --- /dev/null +++ b/addons/web/doc/images/tests2.png diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png Binary files differnew file mode 100644 index 00000000..247f7071 --- /dev/null +++ b/addons/web/doc/images/tests3.png diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst new file mode 100644 index 00000000..5a862d29 --- /dev/null +++ b/addons/web/doc/index.rst @@ -0,0 +1,55 @@ +.. OpenERP Web documentation master file, created by + sphinx-quickstart on Fri Mar 18 16:31:55 2011. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +OpenERP Web Reference Documentation +=================================== + +See also the `OpenERP Web Training`_. + +.. _OpenERP Web Training: https://doc.openerp.com/trunk/training/ + +Basics +------ + +.. toctree:: + :maxdepth: 1 + + module + changelog-7.0 + +Server-Side Web Framework +------------------------- + +.. toctree:: + :maxdepth: 1 + + web_controllers + +Javascript +---------- + +.. toctree:: + :maxdepth: 1 + + guidelines + client_action + testing + +Views +----- + +.. toctree:: + :maxdepth: 1 + + search_view + list_view + form_view + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/addons/web/doc/list_view.rst b/addons/web/doc/list_view.rst new file mode 100644 index 00000000..f73abafe --- /dev/null +++ b/addons/web/doc/list_view.rst @@ -0,0 +1,531 @@ +List View +========= + +Style Hooks +----------- + +The list view provides a few style hook classes for re-styling of list views in +various situations: + +``.oe_list`` + + The root element of the list view, styling rules should be rooted + on that class. + +``table.oe_list_content`` + + The root table for the listview, accessory components may be + generated or added outside this section, this is the list view + "proper". + +``.oe_list_buttons`` + + The action buttons array for the list view, with its sub-elements + + ``.oe_list_add`` + + The default "Create"/"Add" button of the list view + + ``.oe_alternative`` + + The "alternative choice" for the list view, by default text + along the lines of "or import" with a link. + +``.oe_list_field_cell`` + + The cell (``td``) for a given field of the list view, cells which + are *not* fields (e.g. name of a group, or number of items in a + group) will not have this class. The field cell can be further + specified: + + ``.oe_number`` + + Numeric cell types (integer and float) + + ``.oe_button`` + + Action button (``button`` tag in the view) inside the cell + + ``.o_readonly_modifier`` + + Readonly field cell + + ``.oe_list_field_$type`` + + Additional class for the precise type of the cell, ``$type`` + is the field's @widget if there is one, otherwise it's the + field's type. + +``.oe_list_record_selector`` + + Selector cells + +Editable list view +++++++++++++++++++ + +The editable list view module adds a few supplementary style hook +classes, for edition situations: + +``.o_list_editable`` + + Added to the ``.oe_list`` when the list is editable (however that + was done). The class may be removed on-the-fly if the list becomes + non-editable. + +``.oe_editing`` + + Added to both ``.oe_list`` and ``.oe_list_button`` (as the + buttons may be outside of the list view) when a row of the list is + currently being edited. + +``tr.oe_edition`` + + Class set on the row being edited itself. Note that the edition + form is *not* contained within the row, this allows for styling or + modifying the row while it's being edited separately. Mostly for + fields which can not be edited (e.g. read-only fields). + +Columns display customization +----------------------------- + +The list view provides a registry to +:js:class:`openerp.web.list.Column` objects allowing for the +customization of a column's display (e.g. so that a binary field is +rendered as a link to the binary file directly in the list view). + +The registry is ``instance.web.list.columns``, the keys are of the +form ``tag.type`` where ``tag`` can be ``field`` or ``button``, and +``type`` can be either the field's type or the field's ``@widget`` (in +the view). + +Most of the time, you'll want to define a ``tag.widget`` key +(e.g. ``field.progressbar``). + +.. js:class:: openerp.web.list.Column(id, tag, attrs) + + .. js:function:: openerp.web.list.Column.format(record_data, options) + + Top-level formatting method, returns an empty string if the + column is invisible (unless the ``process_modifiers=false`` + option is provided); returns ``options.value_if_empty`` or an + empty string if there is no value in the record for the + column. + + Otherwise calls :js:func:`~openerp.web.list.Column._format` + and returns its result. + + This method only needs to be overridden if the column has no + concept of values (and needs to bypass that check), for a + button for instance. + + Otherwise, custom columns should generally override + :js:func:`~openerp.web.list.Column._format` instead. + + :returns: String + + .. js:function:: openerp.web.list.Column._format(record_data, options) + + Never called directly, called if the column is visible and has + a value. + + The default implementation calls + :js:func:`~openerp.web.format_value` and htmlescapes the + result (via ``_.escape``). + + Note that the implementation of + :js:func:`~openerp.web.list.Column._format` *must* escape the + data provided to it, its output will *not* be escaped by + :js:func:`~openerp.web.list.Column.format`. + + :returns: String + +Editable list view +------------------ + +List view edition is an extension to the base listview providing the +capability of inline record edition by delegating to an embedded form +view. + +Editability status +++++++++++++++++++ + +The editability status of a list view can be queried through the +:js:func:`~openerp.web.ListView.editable` method, will return a falsy +value if the listview is not currently editable. + +The editability status is based on three flags: + +``tree/@editable`` + + If present, can be either ``"top"`` or ``"bottom"``. Either will + make the list view editable, with new records being respectively + created at the top or at the bottom of the view. + +``context.set_editable`` + + Boolean flag extracted from a search context (during the + :js:func:`~openerp.web.ListView.do_search`` handler), ``true`` + will make the view editable (from the top), ``false`` or the + absence of the flag is a noop. + +``defaults.editable`` + + Like ``tree/@editable``, one of absent (``null``)), ``"top"`` or + ``"bottom"``, fallback for the list view if none of the previous + two flags are set. + +These three flags can only *make* a listview editable, they can *not* +override a previously set flag. To do that, a listview user should +instead cancel :ref:`the edit:before event <listview-edit-before>`. + +The editable list view module adds a number of methods to the list +view, on top of implementing the :js:class:`EditorDelegate` protocol: + +Interaction Methods ++++++++++++++++++++ + +.. js:function:: openerp.web.ListView.ensure_saved + + Attempts to resolve the pending edition, if any, by saving the + edited row's current state. + + :returns: delegate resolving to all editions having been saved, or + rejected if a pending edition could not be saved + (e.g. validation failure) + +.. js:function:: openerp.web.ListView.start_edition([record][, options]) + + Starts editing the provided record inline, through an overlay form + view of editable fields in the record. + + If no record is provided, creates a new one according to the + editability configuration of the list view. + + This method resolves any pending edition when invoked, before + starting a new edition. + + :param record: record to edit, or null to create a new record + :type record: :js:class:`~openerp.web.list.Record` + :param EditOptions options: + :returns: delegate to the form used for the edition + +.. js:function:: openerp.web.ListView.save_edition + + Resolves the pending edition. + + :returns: delegate to the save being completed, resolves to an + object with two attributes ``created`` (flag indicating + whether the saved record was just created or was + updated) and ``record`` the reloaded record having been + edited. + +.. js:function:: openerp.web.ListView.cancel_edition([force=false]) + + Cancels pending edition, cleans up the list view in case of + creation (removes the empty record being created). + + :param Boolean force: doesn't check if the user has added any + data, discards the edition unconditionally + +Utility Methods ++++++++++++++++ + +.. js:function:: openerp.web.ListView.get_cells_for(row) + + Extracts the cells from a listview row, and puts them in a + {fieldname: cell} mapping for analysis and manipulation. + + :param jQuery row: + :rtype: Object + +.. js:function:: openerp.web.ListView.with_event(event_name, event, action[, args][, trigger_params]) + + Executes ``action`` in the context of the view's editor, + bracketing it with cancellable event signals. + + :param String event_name: base name for the bracketing event, will + be postfixed by ``:before`` and + ``:after`` before being called + (respectively before and after + ``action`` is executed) + :param Object event: object passed to the ``:before`` event + handlers. + :param Function action: function called with the view's editor as + its ``this``. May return a deferred. + :param Array args: arguments passed to ``action`` + :param Array trigger_params: arguments passed to the ``:after`` + event handler alongside the results + of ``action`` + +Behavioral Customizations ++++++++++++++++++++++++++ + +.. js:function:: openerp.web.ListView.handle_onwrite(record) + + Implements the handling of the ``onwrite`` listview attribute: + calls the RPC methods specified by ``@onwrite``, and if that + method returns an array of ids loads or reloads the records + corresponding to those ids. + + :param record: record being written having triggered the + ``onwrite`` callback + :type record: openerp.web.list.Record + :returns: deferred to all reloadings being done + +Events +++++++ + +For simpler interactions by/with external users of the listview, the +view provides a number of dedicated events to its lifecycle. + +.. note:: if an event is defined as *cancellable*, it means its first + parameter is an object on which the ``cancel`` attribute can + be set. If the ``cancel`` attribute is set, the view will + abort its current behavior as soon as possible, and rollback + any state modification. + + Generally speaking, an event should only be cancelled (by + setting the ``cancel`` flag to ``true``), uncancelling an + event is undefined as event handlers are executed on a + first-come-first-serve basis and later handlers may + re-cancel an uncancelled event. + +.. _listview-edit-before: + +``edit:before`` *cancellable* + + Invoked before the list view starts editing a record. + + Provided with an event object with a single property ``record``, + holding the attributes of the record being edited (``record`` is + empty *but not null* for a new record) + +``edit:after`` + + Invoked after the list view has gone into an edition state, + provided with the attributes of the record being edited (see + ``edit:before``) as first parameter and the form used for the + edition as second parameter. + +``save:before`` *cancellable* + + Invoked right before saving a pending edition, provided with an + event object holding the listview's editor (``editor``) and the + edition form (``form``) + +``save:after`` + + Invoked after a save has been completed + +``cancel:before`` *cancellable* + + Invoked before cancelling a pending edition, provided with the + same information as ``save:before``. + +``cancel:after`` + + Invoked after a pending edition has been cancelled. + +DOM events +++++++++++ + +The list view has grown hooks for the ``keyup`` event on its edition +form (during edition): any such event bubbling out of the edition form +will be forwarded to a method ``keyup_EVENTNAME``, where ``EVENTNAME`` +is the name of the key in ``$.ui.keyCode``. + +The method will also get the event object (originally passed to the +``keyup`` handler) as its sole parameter. + +The base editable list view has handlers for the ``ENTER`` and +``ESCAPE`` keys. + +Editor +------ + +The list-edition modules does not generally interact with the embedded +formview, delegating instead to its +:js:class:`~openerp.web.list.Editor`. + +.. js:class:: openerp.web.list.Editor(parent[, options]) + + The editor object provides a more convenient interface to form + views, and simplifies the usage of form views for semi-arbitrary + edition of stuff. + + However, the editor does *not* task itself with being internally + consistent at this point: calling + e.g. :js:func:`~openerp.web.list.Editor.edit` multiple times in a + row without saving or cancelling each edit is undefined. + + :param parent: + :type parent: :js:class:`~openerp.web.Widget` + :param EditorOptions options: + + .. js:function:: openerp.web.list.Editor.is_editing([record_state]) + + Indicates whether the editor is currently in the process of + providing edition for a record. + + Can be filtered by the state of the record being edited + (whether it's a record being *created* or a record being + *altered*), in which case it asserts both that an edition is + underway and that the record being edited respectively does + not yet exist in the database or already exists there. + + :param record_state: state of the record being edited. + Either ``"new"`` or ``"edit"``. + :type record_state: String + :rtype: Boolean + + .. js:function:: openerp.web.list.Editor.edit(record, configureField[, options]) + + Loads the provided record into the internal form view and + displays the form view. + + Will also attempt to focus the first visible field of the form + view. + + :param Object record: record to load into the form view + (key:value mapping similar to the result + of a ``read``) + :param configureField: function called with each field of the + form view right after the form is + displayed, lets whoever called this + method do some last-minute + configuration of form fields. + :type configureField: Function<String, openerp.web.form.Field> + :param EditOptions options: + :returns: jQuery delegate to the form object + + .. js:function:: openerp.web.list.Editor.save + + Attempts to save the internal form, then hide it + + :returns: delegate to the record under edition (with ``id`` + added for a creation). The record is not updated + from when it was passed in, aside from the ``id`` + attribute. + + .. js:function:: openerp.web.list.Editor.cancel([force=false]) + + Attemps to cancel the edition of the internal form, then hide + the form + + :param Boolean force: unconditionally cancels the edition of + the internal form, even if the user has + already entered data in it. + :returns: delegate to the record under edition + +.. js:class:: EditorOptions + + .. js:attribute:: EditorOptions.formView + + Form view (sub)-class to instantiate and delegate edition to. + + By default, :js:class:`~openerp.web.FormView` + + .. js:attribute:: EditorOptions.delegate + + Object used to get various bits of information about how to + display stuff. + + By default, uses the editor's parent widget. See + :js:class:`EditorDelegate` for the methods and attributes to + provide. + +.. js:class:: EditorDelegate + + Informal protocol defining the methods and attributes expected of + the :js:class:`~openerp.web.list.Editor`'s delegate. + + .. js:attribute:: EditorDelegate.dataset + + The dataset passed to the form view to synchronize the form + view and the outer widget. + + .. js:function:: EditorDelegate.edition_view(editor) + + Called by the :js:class:`~openerp.web.list.Editor` object to + get a form view (JSON) to pass along to the form view it + created. + + The result should be a valid form view, see :doc:`Form Notes + <form_view>` for various peculiarities of the form view + format. + + :param editor: editor object asking for the view + :type editor: :js:class:`~openerp.web.list.Editor` + :returns: form view + :rtype: Object + + .. js:function:: EditorDelegate.prepends_on_create + + By default, the :js:class:`~openerp.web.list.Editor` will + append the ids of newly created records to the + :js:attr:`EditorDelegate.dataset`. If this method returns + ``true``, it will prepend these ids instead. + + :returns: whether new records should be prepended to the + dataset (instead of appended) + :rtype: Boolean + + +.. js:class:: EditOptions + + Options object optionally passed into a method starting an edition + to configure its setup and behavior + + .. js:attribute:: focus_field + + Name of the field to set focus on after setting up the edition + of the record. + + If this option is not provided, or the requested field can not + be focused (invisible, readonly or not in the view), the first + visible non-readonly field is focused. + +Changes from 6.1 +---------------- + +* The editable listview behavior has been rewritten pretty much from + scratch, any code touching on editability will have to be modified + + * The overloading of :js:class:`~openerp.web.ListView.Groups` and + :js:class:`~openerp.web.ListView.List` for editability has been + drastically simplified, and most of the behavior has been moved to + the list view itself. Only + :js:func:`~openerp.web.ListView.List.row_clicked` is still + overridden. + + * A new method ``get_row_for(record) -> jQuery(tr) | null`` has been + added to both ListView.List and ListView.Group, it can be called + from the list view to get the table row matching a record (if such + a row exists). + +* :js:func:`~openerp.web.ListView.do_button_action`'s core behavior + has been split away to + :js:func:`~openerp.web.ListView.handle_button`. This allows bypassing + overrides of :js:func:`~openerp.web.ListView.do_button_action` in a + parent class. + + Ideally, :js:func:`~openerp.web.ListView.handle_button` should not be + overridden. + +* Modifiers handling has been improved (all modifiers information + should now be available through :js:func:`~Column.modifiers_for`, + not just ``invisible``) + +* Changed some handling of the list view's record: a record may now + have no id, and the listview will handle that correctly (for new + records being created) as well as correctly handle the ``id`` being + set. + +* Extended the internal collections structure of the list view with + `#find`_, `#succ`_ and `#pred`_. + +.. _#find: http://underscorejs.org/#find + +.. _#succ: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:succ + +.. _#pred: http://hackage.haskell.org/packages/archive/base/latest/doc/html/Prelude.html#v:pred diff --git a/addons/web/doc/make.bat b/addons/web/doc/make.bat new file mode 100644 index 00000000..3e72cadb --- /dev/null +++ b/addons/web/doc/make.bat @@ -0,0 +1,170 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^<target^>` where ^<target^> is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\OpenERPWeb.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\OpenERPWeb.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end 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 diff --git a/addons/web/doc/module/0 b/addons/web/doc/module/0 new file mode 100644 index 00000000..17b70a86 --- /dev/null +++ b/addons/web/doc/module/0 @@ -0,0 +1,17 @@ +# HG changeset patch +# Parent 0000000000000000000000000000000000000000 + +diff --git a/__init__.py b/__init__.py +new file mode 100644 +diff --git a/__manifest__.py b/__manifest__.py +new file mode 100644 +--- /dev/null ++++ b/__manifest__.py +@@ -0,0 +1,7 @@ ++# __manifest__.py ++{ ++ 'name': "Web Example", ++ 'description': "Basic example of a (future) web module", ++ 'category': 'Hidden', ++ 'depends': ['base'], ++} diff --git a/addons/web/doc/module/10 b/addons/web/doc/module/10 new file mode 100644 index 00000000..dc3b670c --- /dev/null +++ b/addons/web/doc/module/10 @@ -0,0 +1,13 @@ +# HG changeset patch +# Parent 72d9d59a93fcee06ba28cf0b98a1075331dcc8f4 +diff --git a/static/src/css/web_example.css b/static/src/css/web_example.css +new file mode 100644 +--- /dev/null ++++ b/static/src/css/web_example.css +@@ -0,0 +1,6 @@ ++.openerp .oe_web_example { ++ color: white; ++ background-color: black; ++ height: 100%; ++ font-size: 400%; ++} diff --git a/addons/web/doc/module/11 b/addons/web/doc/module/11 new file mode 100644 index 00000000..889ee98e --- /dev/null +++ b/addons/web/doc/module/11 @@ -0,0 +1,11 @@ +# HG changeset patch +# Parent 3ed382d9a8fe64fbb8e2bf4045e3fcd5c74c92bc +diff --git a/__manifest__.py b/__manifest__.py +--- a/__manifest__.py ++++ b/__manifest__.py +@@ -6,4 +6,5 @@ + 'depends': ['web'], + 'data': ['web_example.xml'], + 'js': ['static/src/js/first_module.js'], ++ 'css': ['static/src/css/web_example.css'], + } diff --git a/addons/web/doc/module/12 b/addons/web/doc/module/12 new file mode 100644 index 00000000..85c931c3 --- /dev/null +++ b/addons/web/doc/module/12 @@ -0,0 +1,28 @@ +# HG changeset patch +# Parent 43f21611dacb7c2b2f3810baeeef359ad7c329f0 + +diff --git a/__manifest__.py b/__manifest__.py +--- a/__manifest__.py ++++ b/__manifest__.py +@@ -7,4 +7,5 @@ + 'data': ['web_example.xml'], + 'js': ['static/src/js/first_module.js'], + 'css': ['static/src/css/web_example.css'], ++ 'qweb': ['static/src/xml/web_example.xml'], + } +diff --git a/static/src/xml/web_example.xml b/static/src/xml/web_example.xml +new file mode 100644 +--- /dev/null ++++ b/static/src/xml/web_example.xml +@@ -0,0 +1,11 @@ ++<templates> ++<div t-name="web_example.action" class="oe_web_example oe_web_example_stopped"> ++ <h4 class="oe_web_example_timer">00:00:00</h4> ++ <p class="oe_web_example_start"> ++ <button type="button">Start</button> ++ </p> ++ <p class="oe_web_example_stop"> ++ <button type="button">Stop</button> ++ </p> ++</div> ++</templates> diff --git a/addons/web/doc/module/14 b/addons/web/doc/module/14 new file mode 100644 index 00000000..a908cbc6 --- /dev/null +++ b/addons/web/doc/module/14 @@ -0,0 +1,17 @@ +# HG changeset patch +# Parent ae3b427c96b532794a65357b3f075129cc991276 +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -2,10 +2,6 @@ + openerp.web_example = function (instance) { + instance.web.client_actions.add('example.action', 'instance.web_example.Action'); + instance.web_example.Action = instance.web.Widget.extend({ +- className: 'oe_web_example', +- start: function () { +- this.$el.text("Hello, world!"); +- return this._super(); +- } ++ template: 'web_example.action' + }); + }; diff --git a/addons/web/doc/module/15 b/addons/web/doc/module/15 new file mode 100644 index 00000000..d82abee3 --- /dev/null +++ b/addons/web/doc/module/15 @@ -0,0 +1,19 @@ +# HG changeset patch +# Parent e2d2e1a4cc2d2496aebeb05d94768384427c9e8b +diff --git a/static/src/css/web_example.css b/static/src/css/web_example.css +--- a/static/src/css/web_example.css ++++ b/static/src/css/web_example.css +@@ -2,5 +2,12 @@ + color: white; + background-color: black; + height: 100%; +- font-size: 400%; + } ++.openerp .oe_web_example h4 { ++ margin: 0; ++ font-size: 200%; ++} ++.openerp .oe_web_example.oe_web_example_started .oe_web_example_start button, ++.openerp .oe_web_example.oe_web_example_stopped .oe_web_example_stop button { ++ display: none ++} diff --git a/addons/web/doc/module/16 b/addons/web/doc/module/16 new file mode 100644 index 00000000..816c23aa --- /dev/null +++ b/addons/web/doc/module/16 @@ -0,0 +1,25 @@ +# HG changeset patch +# Parent 2645d7a09dcba7f6d6074a33252c16c03c56fdf3 +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -2,6 +2,18 @@ + openerp.web_example = function (instance) { + instance.web.client_actions.add('example.action', 'instance.web_example.Action'); + instance.web_example.Action = instance.web.Widget.extend({ +- template: 'web_example.action' ++ template: 'web_example.action', ++ events: { ++ 'click .oe_web_example_start button': 'watch_start', ++ 'click .oe_web_example_stop button': 'watch_stop' ++ }, ++ watch_start: function () { ++ this.$el.addClass('oe_web_example_started') ++ .removeClass('oe_web_example_stopped'); ++ }, ++ watch_stop: function () { ++ this.$el.removeClass('oe_web_example_started') ++ .addClass('oe_web_example_stopped'); ++ }, + }); + }; diff --git a/addons/web/doc/module/17 b/addons/web/doc/module/17 new file mode 100644 index 00000000..d6d6ecc7 --- /dev/null +++ b/addons/web/doc/module/17 @@ -0,0 +1,52 @@ +# HG changeset patch +# Parent 2921a545adc3406d3139be7951f3225e94493466 +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -7,13 +7,46 @@ openerp.web_example = function (instance + 'click .oe_web_example_start button': 'watch_start', + 'click .oe_web_example_stop button': 'watch_stop' + }, ++ init: function () { ++ this._super.apply(this, arguments); ++ this._start = null; ++ this._watch = null; ++ }, ++ update_counter: function () { ++ var h, m, s; ++ // Subtracting javascript dates returns the difference in milliseconds ++ var diff = new Date() - this._start; ++ s = diff / 1000; ++ m = Math.floor(s / 60); ++ s -= 60*m; ++ h = Math.floor(m / 60); ++ m -= 60*h; ++ this.$('.oe_web_example_timer').text( ++ _.str.sprintf("%02d:%02d:%02d", h, m, s)); ++ }, + watch_start: function () { + this.$el.addClass('oe_web_example_started') + .removeClass('oe_web_example_stopped'); ++ this._start = new Date(); ++ // Update the UI to the current time ++ this.update_counter(); ++ // Update the counter at 30 FPS (33ms/frame) ++ this._watch = setInterval( ++ this.proxy('update_counter'), ++ 33); + }, + watch_stop: function () { ++ clearInterval(this._watch); ++ this.update_counter(); ++ this._start = this._watch = null; + this.$el.removeClass('oe_web_example_started') + .addClass('oe_web_example_stopped'); + }, ++ destroy: function () { ++ if (this._watch) { ++ clearInterval(this._watch); ++ } ++ this._super(); ++ } + }); + }; diff --git a/addons/web/doc/module/18 b/addons/web/doc/module/18 new file mode 100644 index 00000000..6781c985 --- /dev/null +++ b/addons/web/doc/module/18 @@ -0,0 +1,19 @@ +# HG changeset patch +# Parent e0cc13c2b2ec4d6f6bfdb033b189a32e44106f2e +diff --git a/__init__.py b/__init__.py +--- a/__init__.py ++++ b/__init__.py +@@ -0,0 +1,13 @@ ++# __init__.py ++from openerp.osv import orm, fields ++ ++ ++class Times(orm.Model): ++ _name = 'web_example.stopwatch' ++ ++ _columns = { ++ 'time': fields.integer("Time", required=True, ++ help="Measured time in milliseconds"), ++ 'user_id': fields.many2one('res.users', "User", required=True, ++ help="User who registered the measurement") ++ } diff --git a/addons/web/doc/module/19 b/addons/web/doc/module/19 new file mode 100644 index 00000000..d95a89a3 --- /dev/null +++ b/addons/web/doc/module/19 @@ -0,0 +1,52 @@ +# HG changeset patch +# Parent 05797cc75b49634e640f44b24347f2905b464022 +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -12,11 +12,13 @@ openerp.web_example = function (instance + this._start = null; + this._watch = null; + }, +- update_counter: function () { ++ current: function () { ++ // Subtracting javascript dates returns the difference in milliseconds ++ return new Date() - this._start; ++ }, ++ update_counter: function (time) { + var h, m, s; +- // Subtracting javascript dates returns the difference in milliseconds +- var diff = new Date() - this._start; +- s = diff / 1000; ++ s = time / 1000; + m = Math.floor(s / 60); + s -= 60*m; + h = Math.floor(m / 60); +@@ -29,18 +31,24 @@ openerp.web_example = function (instance + .removeClass('oe_web_example_stopped'); + this._start = new Date(); + // Update the UI to the current time +- this.update_counter(); ++ this.update_counter(this.current()); + // Update the counter at 30 FPS (33ms/frame) +- this._watch = setInterval( +- this.proxy('update_counter'), ++ this._watch = setInterval(function () { ++ this.update_counter(this.current()); ++ }.bind(this), + 33); + }, + watch_stop: function () { + clearInterval(this._watch); +- this.update_counter(); ++ var time = this.current(); ++ this.update_counter(time); + this._start = this._watch = null; + this.$el.removeClass('oe_web_example_started') + .addClass('oe_web_example_stopped'); ++ new instance.web.Model('web_example.stopwatch').call('create', [{ ++ user_id: instance.session.uid, ++ time: time, ++ }]); + }, + destroy: function () { + if (this._watch) { diff --git a/addons/web/doc/module/2 b/addons/web/doc/module/2 new file mode 100644 index 00000000..7d8e9f05 --- /dev/null +++ b/addons/web/doc/module/2 @@ -0,0 +1,12 @@ +# HG changeset patch +# Parent 8a986919a3e22cd7cca51210820c09d4545dc60d +diff --git a/__manifest__.py b/__manifest__.py +--- a/__manifest__.py ++++ b/__manifest__.py +@@ -3,5 +3,5 @@ + 'name': "Web Example", + 'description': "Basic example of a (future) web module", + 'category': 'Hidden', +- 'depends': ['base'], ++ 'depends': ['web'], + } diff --git a/addons/web/doc/module/20 b/addons/web/doc/module/20 new file mode 100644 index 00000000..042ff280 --- /dev/null +++ b/addons/web/doc/module/20 @@ -0,0 +1,64 @@ +Index: web_example/static/src/js/first_module.js +=================================================================== +--- web_example.orig/static/src/js/first_module.js ++++ web_example/static/src/js/first_module.js +@@ -11,20 +11,36 @@ openerp.web_example = function (instance + this._super.apply(this, arguments); + this._start = null; + this._watch = null; ++ this.model = new instance.web.Model('web_example.stopwatch'); ++ }, ++ start: function () { ++ var display = this.display_record.bind(this); ++ return this.model.query() ++ .filter([['user_id', '=', instance.session.uid]]) ++ .all().done(function (records) { ++ _(records).each(display); ++ }); + }, + current: function () { + // Subtracting javascript dates returns the difference in milliseconds + return new Date() - this._start; + }, +- update_counter: function (time) { ++ display_record: function (record) { ++ $('<li>') ++ .text(this.format_time(record.time)) ++ .appendTo(this.$('.oe_web_example_saved')); ++ }, ++ format_time: function (time) { + var h, m, s; + s = time / 1000; + m = Math.floor(s / 60); + s -= 60*m; + h = Math.floor(m / 60); + m -= 60*h; +- this.$('.oe_web_example_timer').text( +- _.str.sprintf("%02d:%02d:%02d", h, m, s)); ++ return _.str.sprintf("%02d:%02d:%02d", h, m, s); ++ }, ++ update_counter: function (time) { ++ this.$('.oe_web_example_timer').text(this.format_time(time)); + }, + watch_start: function () { + this.$el.addClass('oe_web_example_started') +@@ -45,7 +61,7 @@ openerp.web_example = function (instance + this._start = this._watch = null; + this.$el.removeClass('oe_web_example_started') + .addClass('oe_web_example_stopped'); +- new instance.web.Model('web_example.stopwatch').call('create', [{ ++ this.model.call('create', [{ + user_id: instance.session.uid, + time: time, + }]); +Index: web_example/static/src/xml/web_example.xml +=================================================================== +--- web_example.orig/static/src/xml/web_example.xml ++++ web_example/static/src/xml/web_example.xml +@@ -7,5 +7,6 @@ + <p class="oe_web_example_stop"> + <button type="button">Stop</button> + </p> ++ <ol class="oe_web_example_saved"></ol> + </div> + </templates> diff --git a/addons/web/doc/module/21 b/addons/web/doc/module/21 new file mode 100644 index 00000000..0acfac90 --- /dev/null +++ b/addons/web/doc/module/21 @@ -0,0 +1,27 @@ +Index: web_example/static/src/js/first_module.js +=================================================================== +--- web_example.orig/static/src/js/first_module.js ++++ web_example/static/src/js/first_module.js +@@ -55,16 +55,20 @@ openerp.web_example = function (instance + 33); + }, + watch_stop: function () { ++ var self = this; + clearInterval(this._watch); + var time = this.current(); + this.update_counter(time); + this._start = this._watch = null; + this.$el.removeClass('oe_web_example_started') + .addClass('oe_web_example_stopped'); +- this.model.call('create', [{ ++ var record = { + user_id: instance.session.uid, + time: time, +- }]); ++ }; ++ this.model.call('create', [record]).done(function () { ++ self.display_record(record); ++ }); + }, + destroy: function () { + if (this._watch) { diff --git a/addons/web/doc/module/22 b/addons/web/doc/module/22 new file mode 100644 index 00000000..5df76b30 --- /dev/null +++ b/addons/web/doc/module/22 @@ -0,0 +1,6 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- /dev/null ++++ web_example/static/src/tests/timer.js +@@ -0,0 +1 @@ ++ diff --git a/addons/web/doc/module/23 b/addons/web/doc/module/23 new file mode 100644 index 00000000..d08a026a --- /dev/null +++ b/addons/web/doc/module/23 @@ -0,0 +1,14 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- web_example.orig/static/src/tests/timer.js ++++ web_example/static/src/tests/timer.js +@@ -1 +1,8 @@ +- ++openerp.testing.section('timer', function (test) { ++ test('successful test', function () { ++ ok(true, "should work"); ++ }); ++ test('unsuccessful test', function () { ++ ok(false, "shoud fail"); ++ }); ++}); diff --git a/addons/web/doc/module/24 b/addons/web/doc/module/24 new file mode 100644 index 00000000..0972e856 --- /dev/null +++ b/addons/web/doc/module/24 @@ -0,0 +1,10 @@ +Index: web_example/__manifest__.py +=================================================================== +--- web_example.orig/__manifest__.py ++++ web_example/__manifest__.py +@@ -8,4 +8,5 @@ + 'js': ['static/src/js/first_module.js'], + 'css': ['static/src/css/web_example.css'], + 'qweb': ['static/src/xml/web_example.xml'], ++ 'test': ['static/src/tests/timer.js'], + } diff --git a/addons/web/doc/module/25 b/addons/web/doc/module/25 new file mode 100644 index 00000000..1d63dc7f --- /dev/null +++ b/addons/web/doc/module/25 @@ -0,0 +1,55 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- web_example.orig/static/src/tests/timer.js ++++ web_example/static/src/tests/timer.js +@@ -1,8 +1,45 @@ + openerp.testing.section('timer', function (test) { +- test('successful test', function () { +- ok(true, "should work"); +- }); +- test('unsuccessful test', function () { +- ok(false, "shoud fail"); ++ test('format_time', function (instance) { ++ var w = new instance.web_example.Action(); ++ ++ strictEqual( ++ w.format_time(0), ++ '00:00:00'); ++ strictEqual( ++ w.format_time(543), ++ '00:00:00', ++ "should round sub-second times down to zero"); ++ strictEqual( ++ w.format_time(5340), ++ '00:00:05', ++ "should floor sub-second extents to the previous second"); ++ strictEqual( ++ w.format_time(60000), ++ '00:01:00'); ++ strictEqual( ++ w.format_time(3600000), ++ '01:00:00'); ++ strictEqual( ++ w.format_time(86400000), ++ '24:00:00'); ++ strictEqual( ++ w.format_time(604800000), ++ '168:00:00'); ++ ++ strictEqual( ++ w.format_time(22733958), ++ '06:18:53'); ++ strictEqual( ++ w.format_time(41676639), ++ '11:34:36'); ++ strictEqual( ++ w.format_time(57802094), ++ '16:03:22'); ++ strictEqual( ++ w.format_time(73451828), ++ '20:24:11'); ++ strictEqual( ++ w.format_time(84092336), ++ '23:21:32'); + }); + }); diff --git a/addons/web/doc/module/26 b/addons/web/doc/module/26 new file mode 100644 index 00000000..ec0b345a --- /dev/null +++ b/addons/web/doc/module/26 @@ -0,0 +1,38 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- web_example.orig/static/src/tests/timer.js ++++ web_example/static/src/tests/timer.js +@@ -42,4 +42,33 @@ openerp.testing.section('timer', functio + w.format_time(84092336), + '23:21:32'); + }); ++ test('update_counter', function (instance, $fixture) { ++ var w = new instance.web_example.Action(); ++ // $fixture is a DOM tree whose content gets cleaned up before ++ // each test, so we can add whatever we need to it ++ $fixture.append('<div class="oe_web_example_timer">'); ++ // Then set it on the widget ++ w.setElement($fixture); ++ ++ // Update the counter with a known value ++ w.update_counter(22733958); ++ // And check the DOM matches ++ strictEqual($fixture.text(), '06:18:53'); ++ ++ w.update_counter(73451828) ++ strictEqual($fixture.text(), '20:24:11'); ++ }); ++ test('display_record', function (instance, $fixture) { ++ var w = new instance.web_example.Action(); ++ $fixture.append('<ol class="oe_web_example_saved">') ++ w.setElement($fixture); ++ ++ w.display_record({time: 41676639}); ++ w.display_record({time: 84092336}); ++ ++ var $lis = $fixture.find('li'); ++ strictEqual($lis.length, 2, "should have printed 2 records"); ++ strictEqual($lis[0].textContent, '11:34:36'); ++ strictEqual($lis[1].textContent, '23:21:32'); ++ }); + }); diff --git a/addons/web/doc/module/27 b/addons/web/doc/module/27 new file mode 100644 index 00000000..2061b700 --- /dev/null +++ b/addons/web/doc/module/27 @@ -0,0 +1,28 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- web_example.orig/static/src/tests/timer.js ++++ web_example/static/src/tests/timer.js +@@ -71,4 +71,23 @@ openerp.testing.section('timer', functio + strictEqual($lis[0].textContent, '11:34:36'); + strictEqual($lis[1].textContent, '23:21:32'); + }); ++ test('start', {templates: true, rpc: 'mock', asserts: 3}, function (instance, $fixture, mock) { ++ // Rather odd-looking shortcut for search+read in a single RPC call ++ mock('/web/dataset/search_read', function () { ++ // ignore parameters, just return a pair of records. ++ return {records: [ ++ {time: 22733958}, ++ {time: 84092336} ++ ]}; ++ }); ++ ++ var w = new instance.web_example.Action(); ++ return w.appendTo($fixture) ++ .then(function () { ++ var $lis = $fixture.find('li'); ++ strictEqual($lis.length, 2); ++ strictEqual($lis[0].textContent, '06:18:53'); ++ strictEqual($lis[1].textContent, '23:21:32'); ++ }); ++ }); + }); diff --git a/addons/web/doc/module/28 b/addons/web/doc/module/28 new file mode 100644 index 00000000..800e7a6f --- /dev/null +++ b/addons/web/doc/module/28 @@ -0,0 +1,13 @@ +Index: web_example/static/src/js/first_module.js +=================================================================== +--- web_example.orig/static/src/js/first_module.js ++++ web_example/static/src/js/first_module.js +@@ -66,7 +66,7 @@ openerp.web_example = function (instance + user_id: instance.session.uid, + time: time, + }; +- this.model.call('create', [record]).done(function () { ++ return this.model.call('create', [record]).done(function () { + self.display_record(record); + }); + }, diff --git a/addons/web/doc/module/29 b/addons/web/doc/module/29 new file mode 100644 index 00000000..509d4b78 --- /dev/null +++ b/addons/web/doc/module/29 @@ -0,0 +1,37 @@ +Index: web_example/static/src/tests/timer.js +=================================================================== +--- web_example.orig/static/src/tests/timer.js ++++ web_example/static/src/tests/timer.js +@@ -90,4 +90,32 @@ openerp.testing.section('timer', functio + strictEqual($lis[1].textContent, '23:21:32'); + }); + }); ++ test('watch_stop', {templates: true, rpc: 'mock', asserts: 3}, function (instance, $fix, mock) { ++ var created = false; ++ mock('web_example.stopwatch:create', function (args, kwargs) { ++ created = true; ++ // return a fake id (unused) ++ return 42; ++ }); ++ mock('/web/dataset/search_read', function () { ++ return {records: []}; ++ }); ++ ++ var w = new instance.web_example.Action(); ++ return w.appendTo($fix) ++ .then(function () { ++ // Virtual start point 5s before 'now' ++ w._start = new Date() - 5000; ++ return w.watch_stop(); ++ }) ++ .done(function () { ++ ok(created, "should have called create()"); ++ strictEqual($fix.find('.oe_web_example_timer').text(), ++ '00:00:05', ++ "should have updated the timer"); ++ strictEqual($fix.find('li')[0].textContent, ++ '00:00:05', ++ "should have added the new time to the list"); ++ }); ++ }); + }); diff --git a/addons/web/doc/module/3 b/addons/web/doc/module/3 new file mode 100644 index 00000000..c09925f9 --- /dev/null +++ b/addons/web/doc/module/3 @@ -0,0 +1,9 @@ +# HG changeset patch +# Parent dcf661a5eef8f82503831bdb8e6c9d2f9beb285e +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +new file mode 100644 +--- /dev/null ++++ b/static/src/js/first_module.js +@@ -0,0 +1,2 @@ ++// static/src/js/first_module.js ++console.log("Debug statement: file loaded"); diff --git a/addons/web/doc/module/4 b/addons/web/doc/module/4 new file mode 100644 index 00000000..99c073d6 --- /dev/null +++ b/addons/web/doc/module/4 @@ -0,0 +1,11 @@ +# HG changeset patch +# Parent 139dae60de67efa0017f5032f71ab774685c5507 +diff --git a/__manifest__.py b/__manifest__.py +--- a/__manifest__.py ++++ b/__manifest__.py +@@ -4,4 +4,5 @@ + 'description': "Basic example of a (future) web module", + 'category': 'Hidden', + 'depends': ['web'], ++ 'js': ['static/src/js/first_module.js'], + } diff --git a/addons/web/doc/module/5 b/addons/web/doc/module/5 new file mode 100644 index 00000000..49acad9c --- /dev/null +++ b/addons/web/doc/module/5 @@ -0,0 +1,11 @@ +# HG changeset patch +# Parent c8ae7646cce3f271698c844eb2d67f9a8719650d +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -1,2 +1,4 @@ + // static/src/js/first_module.js +-console.log("Debug statement: file loaded"); ++openerp.web_example = function (instance) { ++ console.log("Module loaded"); ++}; diff --git a/addons/web/doc/module/6 b/addons/web/doc/module/6 new file mode 100644 index 00000000..3a1232fa --- /dev/null +++ b/addons/web/doc/module/6 @@ -0,0 +1,29 @@ +# HG changeset patch +# Parent 0026cb80097a724db8d36371bc00da993a51a06f + +diff --git a/__manifest__.py b/__manifest__.py +--- a/__manifest__.py ++++ b/__manifest__.py +@@ -4,5 +4,6 @@ + 'description': "Basic example of a (future) web module", + 'category': 'Hidden', + 'depends': ['web'], ++ 'data': ['web_example.xml'], + 'js': ['static/src/js/first_module.js'], + } +diff --git a/web_example.xml b/web_example.xml +new file mode 100644 +--- /dev/null ++++ b/web_example.xml +@@ -0,0 +1,11 @@ ++<!-- web_example/web_example.xml --> ++<openerp> ++ <data> ++ <record model="ir.actions.client" id="action_client_example"> ++ <field name="name">Example Client Action</field> ++ <field name="tag">example.action</field> ++ </record> ++ <menuitem action="action_client_example" ++ id="menu_client_example"/> ++ </data> ++</openerp> diff --git a/addons/web/doc/module/8 b/addons/web/doc/module/8 new file mode 100644 index 00000000..83e6c371 --- /dev/null +++ b/addons/web/doc/module/8 @@ -0,0 +1,14 @@ +# HG changeset patch +# Parent d987c9edd884de1de30f2ceb70d2e554474b8dd1 +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -1,4 +1,7 @@ + // static/src/js/first_module.js + openerp.web_example = function (instance) { +- console.log("Module loaded"); ++ instance.web.client_actions.add('example.action', 'instance.web_example.action'); ++ instance.web_example.action = function (parent, action) { ++ console.log("Executed the action", action); ++ }; + }; diff --git a/addons/web/doc/module/9 b/addons/web/doc/module/9 new file mode 100644 index 00000000..9113f914 --- /dev/null +++ b/addons/web/doc/module/9 @@ -0,0 +1,21 @@ +# HG changeset patch +# Parent 6a1a7240ea0e63182f60abb1eb5c631089d56dbe +diff --git a/static/src/js/first_module.js b/static/src/js/first_module.js +--- a/static/src/js/first_module.js ++++ b/static/src/js/first_module.js +@@ -1,7 +1,11 @@ + // static/src/js/first_module.js + openerp.web_example = function (instance) { +- instance.web.client_actions.add('example.action', 'instance.web_example.action'); +- instance.web_example.action = function (parent, action) { +- console.log("Executed the action", action); +- }; ++ instance.web.client_actions.add('example.action', 'instance.web_example.Action'); ++ instance.web_example.Action = instance.web.Widget.extend({ ++ className: 'oe_web_example', ++ start: function () { ++ this.$el.text("Hello, world!"); ++ return this._super(); ++ } ++ }); + }; diff --git a/addons/web/doc/module/series b/addons/web/doc/module/series new file mode 100644 index 00000000..ff1a909a --- /dev/null +++ b/addons/web/doc/module/series @@ -0,0 +1,27 @@ +0 +2 +3 +4 +5 +6 +8 +9 +10 +11 +12 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 diff --git a/addons/web/doc/module/testing_0.png b/addons/web/doc/module/testing_0.png Binary files differnew file mode 100644 index 00000000..62711799 --- /dev/null +++ b/addons/web/doc/module/testing_0.png diff --git a/addons/web/doc/module/testing_1.png b/addons/web/doc/module/testing_1.png Binary files differnew file mode 100644 index 00000000..40cf3249 --- /dev/null +++ b/addons/web/doc/module/testing_1.png diff --git a/addons/web/doc/search_view.rst b/addons/web/doc/search_view.rst new file mode 100644 index 00000000..76692a0d --- /dev/null +++ b/addons/web/doc/search_view.rst @@ -0,0 +1,560 @@ +Search View +=========== + +OpenERP Web 7.0 implements a unified facets-based search view instead +of the previous form-like search view (composed of buttons and +multiple fields). The goal for this change is twofold: + +* Avoid the common issue of users confusing the search view with a + form view and trying to create their records through it (or entering + all their data, hitting the ``Create`` button expecting their record + to be created and losing everything). + +* Improve the looks and behaviors of the view, and the fit within + OpenERP Web's new design. + +The internal structure of the faceted search is inspired by +`VisualSearch <http://documentcloud.github.com/visualsearch/>`_ +[#previous]_. + +As does VisualSearch, the new search view is based on `Backbone`_ and +makes significant use of Backbone's models and collections (OpenERP +Web's widgets make a good replacement for Backbone's own views). As a +result, understanding the implementation details of the OpenERP Web 7 +search view also requires a basic understanding of Backbone's models, +collections and events. + +.. note:: + + This document may mention *fetching* data. This is a shortcut for + "returning a :js:class:`Deferred` to [whatever is being + fetched]". Unless further noted, the function or method may opt to + return nothing by fetching ``null`` (which can easily be done by + returning ``$.when(null)``, which simply wraps the ``null`` in a + Deferred). + +Working with the search view: creating new inputs +------------------------------------------------- + +The primary component of search views, as with all other OpenERP +views, are inputs. The search view has two types of inputs — filters +and fields — but only one is easly customizable: fields. + +The mapping from OpenERP field types (and widgets) to search view +objects is stored in the ``openerp.web.search.fields`` +:js:class:`~openerp.web.Registry` where new field types and widgets +can be added. + +Search view inputs have four main roles: + +Loading defaults +++++++++++++++++ + +Once the search view has initialized all its inputs, it will call +:js:func:`~openerp.web.search.Input.facet_for_defaults` on each input, +passing it a mapping (a javascript object) of ``name:value`` extracted +from the action's context. + +This method should fetch a :js:class:`~openerp.web.search.Facet` (or +an equivalent object) for the field's default value if applicable (if +a default value for the field is found in the ``defaults`` mapping). + +A default implementation is provided which checks if ``defaults`` +contains a non-falsy value for the field's ``@name`` and calls +:js:func:`openerp.web.search.Input.facet_for` with that value. + +There is no default implementation of +:js:func:`openerp.web.search.Input.facet_for` [#no_impl]_, but +:js:class:`openerp.web.search.Field` provides one, which uses the +value as-is to fetch a :js:class:`~openerp.web.search.Facet`. + +Providing completions ++++++++++++++++++++++ + +An important component of the new search view is the auto-completion +pane, and the task of providing completion items is delegated to +inputs through the :js:func:`~openerp.web.search.Input.complete` +method. + +This method should take a single argument (the string being typed by +the user) and should fetch an ``Array`` of possible completions +[#completion]_. + +A default implementation is provided which fetches nothing. + +A completion item is a javascript object with two keys (technically it +can have any number of keys, but only these two will be used by the +search view): + +``label`` + + The string which will be displayed in the completion pane. It may + be formatted using HTML (inline only), as a result if ``value`` is + interpolated into it it *must* be escaped. ``_.escape`` can be + used for this. + +``facet`` + + Either a :js:class:`~openerp.web.search.Facet` object or (more + commonly) the corresponding attributes object. This is the facet + which will be inserted into the search query if the completion + item is selected by the user. + +If the ``facet`` is not provided (not present, ``null``, ``undefined`` +or any other falsy value), the completion item will not be selectable +and will act as a section title of sort (the ``label`` will be +formatted differently). If an input *may* fetch multiple completion +items, it *should* prefix those with a section title using its own +name. This has no technical consequence but is clearer for users. + +.. note:: + + If a field is :js:func:`invisible + <openerp.web.search.Input.visible>`, its completion function will + *not* be called. + +Providing drawer/supplementary UI ++++++++++++++++++++++++++++++++++ + +For some inputs (fields or not), interaction via autocompletion may be +awkward or even impossible. + +These may opt to being rendered in a "drawer" as well or instead. In +that case, they will undergo the normal widget lifecycle and be +rendered inside the drawer. + +.. Found no good type-based way to handle this, since there is no MI + (so no type-tagging) and it's possible for both Field and non-Field + input to be put into the drawer, for whatever reason (e.g. some + sort of auto-detector completion item for date widgets, but a + second more usual calendar widget in the drawer for more + obvious/precise interactions) + +Any input can note its desire to be rendered in the drawer by +returning a truthy value from +:js:func:`~openerp.web.search.Input.in_drawer`. + +By default, :js:func:`~openerp.web.search.Input.in_drawer` returns the +value of :js:attr:`~openerp.web.search.Input._in_drawer`, which is +``false``. The behavior can be toggled either by redefining the +attribute to ``true`` (either on the class or on the input), or by +overriding :js:func:`~openerp.web.search.Input.in_drawer` itself. + +The input will be rendered in the full width of the drawer, it will be +started only once (per view). + +.. todo:: drawer API (if a widget wants to close the drawer in some + way), part of the low-level SearchView API/interactions? + + +.. todo:: handle filters and filter groups via a "driver" input which + dynamically collects, lays out and renders filters? => + exercises drawer thingies + +.. note:: + + An :js:func:`invisible <openerp.web.search.Input.visible>` input + will not be inserted into the drawer. + +Converting from facet objects ++++++++++++++++++++++++++++++ + +Ultimately, the point of the search view is to allow searching. In +OpenERP this is done via :ref:`domains <openerpserver:domains>`. On +the other hand, the OpenERP Web 7 search view's state is modelled +after a collection of :js:class:`~openerp.web.search.Facet`, and each +field of a search view may have special requirements when it comes to +the domains it produces [#special]_. + +So there needs to be some way of mapping +:js:class:`~openerp.web.search.Facet` objects to OpenERP search data. + +This is done via an input's +:js:func:`~openerp.web.search.Input.get_domain` and +:js:func:`~openerp.web.search.Input.get_context`. Each takes a +:js:class:`~openerp.web.search.Facet` and returns whatever it's +supposed to generate (a domain or a context, respectively). Either can +return ``null`` if the current value does not map to a domain or +context, and can throw an :js:class:`~openerp.web.search.Invalid` +exception if the value is not valid at all for the field. + +.. note:: + + The :js:class:`~openerp.web.search.Facet` object can have any + number of values (from 1 upwards) + +.. note:: + + There is a third conversion method, + :js:func:`~openerp.web.search.Input.get_groupby`, which returns an + ``Array`` of groupby domains rather than a single context. At this + point, it is only implemented on (and used by) filters. + +Programmatic interactions: internal model +----------------------------------------- + +This new searchview is built around an instance of +:js:class:`~openerp.web.search.SearchQuery` available as +:js:attr:`openerp.web.SearchView.query`. + +The query is a `backbone collection`_ of +:js:class:`~openerp.web.search.Facet` objects, which can be interacted +with directly by external objects or search view controls +(e.g. widgets displayed in the drawer). + +.. js:class:: openerp.web.search.SearchQuery + + The current search query of the search view, provides convenience + behaviors for manipulating :js:class:`~openerp.web.search.Facet` + on top of the usual `backbone collection`_ methods. + + The query ensures all of its facets contain at least one + :js:class:`~openerp.web.search.FacetValue` instance. Otherwise, + the facet is automatically removed from the query. + + .. js:function:: openerp.web.search.SearchQuery.add(values, options) + + Overridden from the base ``add`` method so that adding a facet + which is *already* in the collection will merge the value of + the new facet into the old one rather than add a second facet + with different values. + + :param values: facet, facet attributes or array thereof + :returns: the collection itself + + .. js:function:: openerp.web.search.SearchQuery.toggle(value, options) + + Convenience method for toggling facet values in a query: + removes the values (through the facet itself) if they are + present, adds them if they are not. If the facet itself is not + in the collection, adds it automatically. + + A toggling is atomic: only one change event will be triggered + on the facet regardless of the number of values added to or + removed from the facet (if the facet already exists), and the + facet is only removed from the query if it has no value *at + the end* of the toggling. + + :param value: facet or facet attributes + :returns: the collection + +.. js:class:: openerp.web.search.Facet + + A `backbone model`_ representing a single facet of the current + search. May map to a search field, or to a more complex or + fuzzier input (e.g. a custom filter or an advanced search). + + .. js:attribute:: category + + The displayed name of the facet, as a ``String``. This is a + backbone model attribute. + + .. js:attribute:: field + + The :js:class:`~openerp.web.search.Input` instance which + originally created the facet [#facet-field]_, used to delegate + some operations (such as serializing the facet's values to + domains and contexts). This is a backbone model attribute. + + .. js:attribute:: values + + :js:class:`~openerp.web.search.FacetValues` as a javascript + attribute, stores all the values for the facet and helps + propagate their events to the facet. Is also available as a + backbone attribute (via ``#get`` and ``#set``) in which cases + it serializes to and deserializes from javascript arrays (via + ``Collection#toJSON`` and ``Collection#reset``). + + .. js:attribute:: [icon] + + optional, a single ASCII letter (a-z or A-Z) mapping to the + bundled mnmliconsRegular icon font. + + When a facet with an ``icon`` attribute is rendered, the icon + is displayed (in the icon font) in the first section of the + facet instead of the ``category``. + + By default, only filters make use of this facility. + +.. js:class:: openerp.web.search.FacetValues + + `Backbone collection`_ of + :js:class:`~openerp.web.search.FacetValue` instances. + +.. js:class:: openerp.web.search.FacetValue + + `Backbone model`_ representing a single value within a facet, + represents a pair of (displayed name, logical value). + + .. js:attribute:: label + + Backbone model attribute storing the "displayable" + representation of the value, visually output to the + user. Must be a string. + + .. js:attribute:: value + + Backbone model attribute storing the logical/internal value + (of itself), will be used by + :js:class:`~openerp.web.search.Input` to serialize to domains + and contexts. + + Can be of any type. + +Field services +-------------- + +:js:class:`~openerp.web.search.Field` provides a default +implementation of :js:func:`~openerp.web.search.Input.get_domain` and +:js:func:`~openerp.web.search.Input.get_context` taking care of most +of the peculiarities pertaining to OpenERP's handling of fields in +search views. It also provides finer hooks to let developers of new +fields and widgets customize the behavior they want without +necessarily having to reimplement all of +:js:func:`~openerp.web.search.Input.get_domain` or +:js:func:`~openerp.web.search.Input.get_context`: + +.. js:function:: openerp.web.search.Field.get_context(facet) + + If the field has no ``@context``, simply returns + ``null``. Otherwise, calls + :js:func:`~openerp.web.search.Field.value_from` once for each + :js:class:`~openerp.web.search.FacetValue` of the current + :js:class:`~openerp.web.search.Facet` (in order to extract the + basic javascript object from the + :js:class:`~openerp.web.search.FacetValue` then evaluates + ``@context`` with each of these values set as ``self``, and + returns the union of all these contexts. + + :param facet: + :type facet: openerp.web.search.Facet + :returns: a context (literal or compound) + +.. js:function:: openerp.web.search.Field.get_domain(facet) + + If the field has no ``@filter_domain``, calls + :js:func:`~openerp.web.search.Field.make_domain` once with each + :js:class:`~openerp.web.search.FacetValue` of the current + :js:class:`~openerp.web.search.Facet` as well as the field's + ``@name`` and either its ``@operator`` or + :js:attr:`~openerp.web.search.Field.default_operator`. + + If the field has an ``@filter_value``, calls + :js:func:`~openerp.web.search.Field.value_from` once per + :js:class:`~openerp.web.search.FacetValue` and evaluates + ``@filter_value`` with each of these values set as ``self``. + + In either case, "ors" all of the resulting domains (using ``|``) + if there is more than one + :js:class:`~openerp.web.search.FacetValue` and returns the union + of the result. + + :param facet: + :type facet: openerp.web.search.Facet + :returns: a domain (literal or compound) + +.. js:function:: openerp.web.search.Field.make_domain(name, operator, facetValue) + + Builds a literal domain from the provided data. Calls + :js:func:`~openerp.web.search.Field.value_from` on the + :js:class:`~openerp.web.search.FacetValue` and evaluates and sets + it as the domain's third value, uses the other two parameters as + the first two values. + + Can be overridden to build more complex default domains. + + :param String name: the field's name + :param String operator: the operator to use in the field's domain + :param facetValue: + :type facetValue: openerp.web.search.FacetValue + :returns: Array<(String, String, Object)> + +.. js:function:: openerp.web.search.Field.value_from(facetValue) + + Extracts a "bare" javascript value from the provided + :js:class:`~openerp.web.search.FacetValue`, and returns it. + + The default implementation will simply return the ``value`` + backbone property of the argument. + + :param facetValue: + :type facetValue: openerp.web.search.FacetValue + :returns: Object + +.. js:attribute:: openerp.web.search.Field.default_operator + + Operator used to build a domain when a field has no ``@operator`` + or ``@filter_domain``. ``"="`` for + :js:class:`~openerp.web.search.Field` + +Arbitrary data storage +---------------------- + +:js:class:`~openerp.web.search.Facet` and +:js:class:`~openerp.web.search.FacetValue` objects (and structures) +provided by your widgets should never be altered by the search view +(or an other widget). This means you are free to add arbitrary fields +in these structures if you need to (because you have more complex +needs than the attributes described in this document). + +Ideally this should be avoided, but the possibility remains. + +Changes +------- + +.. todo:: merge in changelog instead? + +The displaying of the search view was significantly altered from +OpenERP Web 6.1 to OpenERP Web 7. + +As a result, while the external API used to interact with the search +view does not change many internal details — including the interaction +between the search view and its widgets — were significantly altered: + +Internal operations ++++++++++++++++++++ + +* :js:func:`openerp.web.SearchView.do_clear` has been removed +* :js:func:`openerp.web.SearchView.do_toggle_filter` has been removed + +Widgets API ++++++++++++ + +* :js:func:`openerp.web.search.Widget.render` has been removed + +* :js:func:`openerp.web.search.Widget.make_id` has been removed + +* Search field objects are not openerp widgets anymore, their + ``start`` is not generally called + +* :js:func:`~openerp.web.search.Input.clear` has been removed since + clearing the search view now simply consists of removing all search + facets + +* :js:func:`~openerp.web.search.Input.get_domain` and + :js:func:`~openerp.web.search.Input.get_context` now take a + :js:class:`~openerp.web.search.Facet` as parameter, from which it's + their job to get whatever value they want + +* :js:func:`~openerp.web.search.Input.get_groupby` has been added. It returns + an :js:class:`Array` of context-like constructs. By default, it does not do + anything in :js:class:`~openerp.web.search.Field` and it returns the various + contexts of its enabled filters in + :js:class:`~openerp.web.search.FilterGroup`. + +Filters ++++++++ + +* :js:func:`openerp.web.search.Filter.is_enabled` has been removed + +* :js:class:`~openerp.web.search.FilterGroup` instances are still + rendered (and started) in the "advanced search" drawer. + +Fields +++++++ + +* ``get_value`` has been replaced by + :js:func:`~openerp.web.search.Field.value_from` as it now takes a + :js:class:`~openerp.web.search.FacetValue` argument (instead of no + argument). It provides a default implementation returning the + ``value`` property of its argument. + +* The third argument to + :js:func:`~openerp.web.search.Field.make_domain` is now a + :js:class:`~openerp.web.search.FacetValue` so child classes have all + the information they need to derive the "right" resulting domain. + +Custom filters +++++++++++++++ + +Instead of being an intrinsic part of the search view, custom filters +are now a special case of filter groups. They are treated specially +still, but much less so than they used to be. + +Many To One ++++++++++++ + +* Because the autocompletion service is now provided by the search + view itself, + :js:func:`openerp.web.search.ManyToOneField.setup_autocomplete` has + been removed. + +Advanced Search ++++++++++++++++ + +* The advanced search is now a more standard + :js:class:`~openerp.web.search.Input` configured to be rendered in + the drawer. + +* :js:class:`~openerp.web.search.ExtendedSearchProposition.Field` are + now standard widgets, with the "right" behaviors (they don't rebind + their ``$element`` in ``start()``) + +* The ad-hoc optional setting of the openerp field descriptor on a + :js:class:`~openerp.web.search.ExtendedSearchProposition.Field` has + been removed, the field descriptor is now passed as second argument + to the + :js:class:`~openerp.web.search.ExtendedSearchProposition.Field`'s + constructor, and bound to its + :js:attr:`~openerp.web.search.ExtendedSearchProposition.Field.field`. + +* Instead of its former domain triplet ``(field, operator, value)``, + :js:func:`~openerp.web.search.ExtendedSearchProposition.get_proposition` + now returns an object with two fields ``label`` and ``value``, + respectively a human-readable version of the proposition and the + corresponding domain triplet for the proposition. + +.. [#previous] + + the original view was implemented on top of a monkey-patched + VisualSearch, but as our needs diverged from VisualSearch's goal + this made less and less sense ultimately leading to a clean-room + reimplementation + +.. [#no_impl] + + In case you are extending the search view with a brand new type of + input + +.. [#completion] + + Ideally this array should not hold more than about 10 items, but + the search view does not put any constraint on this at the + moment. Note that this may change. + +.. [#facet-field] + + ``field`` does not actually need to be an instance of + :js:class:`~openerp.web.search.Input`, nor does it need to be what + created the facet, it just needs to provide the three + facet-serialization methods + :js:func:`~openerp.web.search.Input.get_domain`, + :js:func:`~openerp.web.search.Input.get_context` and + :js:func:`~openerp.web.search.Input.get_gropuby`, existing + :js:class:`~openerp.web.search.Input` subtypes merely provide + convenient base implementation for those methods. + + Complex search view inputs (especially those living in the drawer) + may prefer using object literals with the right slots returning + closed-over values or some other scheme un-bound to an actual + :js:class:`~openerp.web.search.Input`, as + :js:class:`~openerp.web.search.CustomFilters` and + :js:class:`~openerp.web.search.Advanced` do. + +.. [#special] + + search view fields may also bundle context data to add to the + search context + +.. _Backbone: + http://documentcloud.github.com/backbone/ + +.. _Backbone.Collection: +.. _Backbone collection: + http://documentcloud.github.com/backbone/#Collection + +.. _Backbone model: + http://documentcloud.github.com/backbone/#Model + +.. _commit 3fca87101d: + https://github.com/documentcloud/visualsearch/commit/3fca87101d |
