[Merge]Merge with trunk
bzr revid: vja@tinyerp.com-20130501054129-2ie8kj73yosxbfo8
This commit is contained in:
commit
3b7863b479
|
@ -13,6 +13,7 @@ This module provides the core of the OpenERP Web Client.
|
|||
'auto_install': True,
|
||||
'post_load': 'wsgi_postload',
|
||||
'js' : [
|
||||
"static/src/fixbind.js",
|
||||
"static/lib/datejs/globalization/en-US.js",
|
||||
"static/lib/datejs/core.js",
|
||||
"static/lib/datejs/parser.js",
|
||||
|
@ -26,6 +27,7 @@ This module provides the core of the OpenERP Web Client.
|
|||
"static/lib/spinjs/spin.js",
|
||||
"static/lib/jquery.autosize/jquery.autosize.js",
|
||||
"static/lib/jquery.blockUI/jquery.blockUI.js",
|
||||
"static/lib/jquery.placeholder/jquery.placeholder.js",
|
||||
"static/lib/jquery.ui/js/jquery-ui-1.9.1.custom.js",
|
||||
"static/lib/jquery.ui.timepicker/js/jquery-ui-timepicker-addon.js",
|
||||
"static/lib/jquery.ui.notify/js/jquery.notify.js",
|
||||
|
@ -75,6 +77,7 @@ This module provides the core of the OpenERP Web Client.
|
|||
"static/test/class.js",
|
||||
"static/test/registry.js",
|
||||
"static/test/form.js",
|
||||
"static/test/data.js",
|
||||
"static/test/list-utils.js",
|
||||
"static/test/formats.js",
|
||||
"static/test/rpc.js",
|
||||
|
|
|
@ -15,6 +15,8 @@ import simplejson
|
|||
import time
|
||||
import urllib
|
||||
import urllib2
|
||||
import urlparse
|
||||
import xmlrpclib
|
||||
import zlib
|
||||
from xml.etree import ElementTree
|
||||
from cStringIO import StringIO
|
||||
|
@ -91,16 +93,50 @@ def db_list(req):
|
|||
dbs = [i for i in dbs if re.match(r, i)]
|
||||
return dbs
|
||||
|
||||
def db_monodb(req):
|
||||
# if only one db exists, return it else return False
|
||||
def db_monodb_redirect(req):
|
||||
db = False
|
||||
redirect = False
|
||||
|
||||
# 1 try the db in the url
|
||||
db_url = req.params.get('db')
|
||||
if db_url:
|
||||
return (db_url, False)
|
||||
|
||||
try:
|
||||
dbs = db_list(req)
|
||||
if len(dbs) == 1:
|
||||
return dbs[0]
|
||||
except Exception:
|
||||
# ignore access denied
|
||||
pass
|
||||
return False
|
||||
dbs = []
|
||||
|
||||
# 2 use the database from the cookie if it's listable and still listed
|
||||
cookie_db = req.httprequest.cookies.get('last_used_database')
|
||||
if cookie_db in dbs:
|
||||
db = cookie_db
|
||||
|
||||
# 3 use the first db
|
||||
if dbs and not db:
|
||||
db = dbs[0]
|
||||
|
||||
# redirect to the chosen db if multiple are available
|
||||
if db and len(dbs) > 1:
|
||||
query = dict(urlparse.parse_qsl(req.httprequest.query_string, keep_blank_values=True))
|
||||
query.update({ 'db': db })
|
||||
redirect = req.httprequest.path + '?' + urllib.urlencode(query)
|
||||
return (db, redirect)
|
||||
|
||||
def db_monodb(req):
|
||||
# if only one db exists, return it else return False
|
||||
return db_monodb_redirect(req)[0]
|
||||
|
||||
def redirect_with_hash(req, url, code=303):
|
||||
if req.httprequest.user_agent.browser == 'msie':
|
||||
try:
|
||||
version = float(req.httprequest.user_agent.version)
|
||||
if version < 10:
|
||||
return "<html><head><script>window.location = '%s#' + location.hash;</script></head></html>" % url
|
||||
except Exception:
|
||||
pass
|
||||
return werkzeug.utils.redirect(url, code)
|
||||
|
||||
def module_topological_sort(modules):
|
||||
""" Return a list of module names sorted so that their dependencies of the
|
||||
|
@ -291,6 +327,10 @@ def manifest_glob(req, extension, addons=None, db=None):
|
|||
return r
|
||||
|
||||
def manifest_list(req, extension, mods=None, db=None):
|
||||
""" list ressources to load specifying either:
|
||||
mods: a comma separated string listing modules
|
||||
db: a database name (return all installed modules in that database)
|
||||
"""
|
||||
if not req.debug:
|
||||
path = '/web/webclient/' + extension
|
||||
if mods is not None:
|
||||
|
@ -299,12 +339,7 @@ def manifest_list(req, extension, mods=None, db=None):
|
|||
path += '?' + urllib.urlencode({'db': db})
|
||||
return [path]
|
||||
files = manifest_glob(req, extension, addons=mods, db=db)
|
||||
i_am_diabetic = req.httprequest.environ["QUERY_STRING"].count("no_sugar") >= 1 or \
|
||||
req.httprequest.environ.get('HTTP_REFERER', '').count("no_sugar") >= 1
|
||||
if i_am_diabetic:
|
||||
return [wp for _fp, wp in files]
|
||||
else:
|
||||
return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in files]
|
||||
return [wp for _fp, wp in files]
|
||||
|
||||
def get_last_modified(files):
|
||||
""" Returns the modification time of the most recently modified
|
||||
|
@ -534,6 +569,10 @@ class Home(openerpweb.Controller):
|
|||
|
||||
@openerpweb.httprequest
|
||||
def index(self, req, s_action=None, db=None, **kw):
|
||||
db, redir = db_monodb_redirect(req)
|
||||
if redir:
|
||||
return redirect_with_hash(req, redir)
|
||||
|
||||
js = "\n ".join('<script type="text/javascript" src="%s"></script>' % i for i in manifest_list(req, 'js', db=db))
|
||||
css = "\n ".join('<link rel="stylesheet" href="%s">' % i for i in manifest_list(req, 'css', db=db))
|
||||
|
||||
|
@ -604,6 +643,18 @@ class WebClient(openerpweb.Controller):
|
|||
|
||||
content, checksum = concat_files((f[0] for f in files), reader)
|
||||
|
||||
# move up all @import and @charset rules to the top
|
||||
matches = []
|
||||
def push(matchobj):
|
||||
matches.append(matchobj.group(0))
|
||||
return ''
|
||||
|
||||
content = re.sub(re.compile("(@charset.+;$)", re.M), push, content)
|
||||
content = re.sub(re.compile("(@import.+;$)", re.M), push, content)
|
||||
|
||||
matches.append(content)
|
||||
content = '\n'.join(matches)
|
||||
|
||||
return make_conditional(
|
||||
req, req.make_response(content, [('Content-Type', 'text/css')]),
|
||||
last_modified, checksum)
|
||||
|
@ -869,7 +920,7 @@ class Session(openerpweb.Controller):
|
|||
"""
|
||||
saved_actions = req.httpsession.get('saved_actions')
|
||||
if not saved_actions:
|
||||
saved_actions = {"next":0, "actions":{}}
|
||||
saved_actions = {"next":1, "actions":{}}
|
||||
req.httpsession['saved_actions'] = saved_actions
|
||||
# we don't allow more than 10 stored actions
|
||||
if len(saved_actions["actions"]) >= 10:
|
||||
|
@ -1324,19 +1375,30 @@ class Binary(openerpweb.Controller):
|
|||
elif dbname is None:
|
||||
dbname = db_monodb(req)
|
||||
|
||||
if uid is None:
|
||||
if not uid:
|
||||
uid = openerp.SUPERUSER_ID
|
||||
|
||||
if not dbname:
|
||||
image_data = self.placeholder(req, 'logo.png')
|
||||
else:
|
||||
registry = openerp.modules.registry.RegistryManager.get(dbname)
|
||||
with registry.cursor() as cr:
|
||||
user = registry.get('res.users').browse(cr, uid, uid)
|
||||
if user.company_id.logo_web:
|
||||
image_data = user.company_id.logo_web.decode('base64')
|
||||
else:
|
||||
image_data = self.placeholder(req, 'nologo.png')
|
||||
try:
|
||||
# create an empty registry
|
||||
registry = openerp.modules.registry.Registry(dbname.lower())
|
||||
with registry.cursor() as cr:
|
||||
cr.execute("""SELECT c.logo_web
|
||||
FROM res_users u
|
||||
LEFT JOIN res_company c
|
||||
ON c.id = u.company_id
|
||||
WHERE u.id = %s
|
||||
""", (uid,))
|
||||
row = cr.fetchone()
|
||||
if row and row[0]:
|
||||
image_data = str(row[0]).decode('base64')
|
||||
else:
|
||||
image_data = self.placeholder(req, 'nologo.png')
|
||||
except Exception:
|
||||
image_data = self.placeholder(req, 'logo.png')
|
||||
|
||||
headers = [
|
||||
('Content-Type', 'image/png'),
|
||||
('Content-Length', len(image_data)),
|
||||
|
@ -1381,7 +1443,7 @@ class Action(openerpweb.Controller):
|
|||
else:
|
||||
return False
|
||||
|
||||
class Export(View):
|
||||
class Export(openerpweb.Controller):
|
||||
_cp_path = "/web/export"
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
|
@ -1522,7 +1584,7 @@ class Export(View):
|
|||
(prefix + '/' + k, prefix_string + '/' + v)
|
||||
for k, v in self.fields_info(req, model, export_fields).iteritems())
|
||||
|
||||
#noinspection PyPropertyDefinition
|
||||
class ExportFormat(object):
|
||||
@property
|
||||
def content_type(self):
|
||||
""" Provides the format's content type """
|
||||
|
@ -1570,7 +1632,7 @@ class Export(View):
|
|||
('Content-Type', self.content_type)],
|
||||
cookies={'fileToken': int(token)})
|
||||
|
||||
class CSVExport(Export):
|
||||
class CSVExport(ExportFormat, http.Controller):
|
||||
_cp_path = '/web/export/csv'
|
||||
fmt = {'tag': 'csv', 'label': 'CSV'}
|
||||
|
||||
|
@ -1605,7 +1667,7 @@ class CSVExport(Export):
|
|||
fp.close()
|
||||
return data
|
||||
|
||||
class ExcelExport(Export):
|
||||
class ExcelExport(ExportFormat, http.Controller):
|
||||
_cp_path = '/web/export/xls'
|
||||
fmt = {
|
||||
'tag': 'xls',
|
||||
|
@ -1644,7 +1706,7 @@ class ExcelExport(Export):
|
|||
fp.close()
|
||||
return data
|
||||
|
||||
class Reports(View):
|
||||
class Reports(openerpweb.Controller):
|
||||
_cp_path = "/web/report"
|
||||
POLLING_DELAY = 0.25
|
||||
TYPES_MAPPING = {
|
||||
|
|
|
@ -27,7 +27,11 @@ sys.path.insert(0, os.path.abspath('..'))
|
|||
|
||||
# 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']
|
||||
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']
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
.. _module:
|
||||
|
||||
.. queue:: module/series
|
||||
|
||||
Building an OpenERP Web module
|
||||
==============================
|
||||
|
||||
|
@ -19,8 +21,7 @@ A very basic OpenERP module structure will be our starting point:
|
|||
├── __init__.py
|
||||
└── __openerp__.py
|
||||
|
||||
.. literalinclude:: module/__openerp__.py
|
||||
:language: python
|
||||
.. patch::
|
||||
|
||||
This is a sufficient minimal declaration of a valid OpenERP module.
|
||||
|
||||
|
@ -41,8 +42,7 @@ module is automatically recognized as "web-enabled" if it contains a
|
|||
is the extent of it. You should also change the dependency to list
|
||||
``web``:
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.1.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -67,15 +67,13 @@ 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.
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js
|
||||
:language: javascript
|
||||
.. 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):
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.2.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
At this point, if the module is installed and the client reloaded the
|
||||
message should appear in your browser's development console.
|
||||
|
@ -100,8 +98,7 @@ 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`_:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.1.diff
|
||||
:language: diff
|
||||
.. 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:
|
||||
|
@ -122,17 +119,12 @@ To demonstrate, let's build a simple :doc:`client action
|
|||
|
||||
First, the action declaration:
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.3.diff
|
||||
:language: diff
|
||||
|
||||
.. literalinclude:: module/web_example.xml
|
||||
:language: xml
|
||||
.. patch::
|
||||
|
||||
then set up the :doc:`client action hook <client_action>` to register
|
||||
a function (for now):
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.2.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
Updating the module (in order to load the XML description) and
|
||||
re-starting the server should display a new menu *Example Client
|
||||
|
@ -148,8 +140,7 @@ 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:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.3.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
after reloading the client (to update the javascript file), instead of
|
||||
printing to the console the menu item clears the whole screen and
|
||||
|
@ -159,15 +150,13 @@ 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:
|
||||
|
||||
.. literalinclude:: module/static/src/css/web_example.css
|
||||
:language: css
|
||||
.. 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):
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.4.diff
|
||||
:language: diff
|
||||
.. 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,
|
||||
|
@ -204,22 +193,16 @@ integration to OpenERP Web widgets.
|
|||
|
||||
Adding a template file is similar to adding a style sheet:
|
||||
|
||||
.. literalinclude:: module/static/src/xml/web_example.xml
|
||||
:language: xml
|
||||
|
||||
.. literalinclude:: module/__openerp__.py.5.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
The template can then easily be hooked in the widget:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.4.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
And finally the CSS can be altered to style the new (and more complex)
|
||||
template-generated DOM, rather than the code-generated one:
|
||||
|
||||
.. literalinclude:: module/static/src/css/web_example.css.1.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
.. note::
|
||||
|
||||
|
@ -238,15 +221,13 @@ 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:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.5.diff
|
||||
:language: diff
|
||||
.. 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:
|
||||
|
||||
.. literalinclude:: module/static/src/js/first_module.js.6.diff
|
||||
:language: diff
|
||||
.. patch::
|
||||
|
||||
* An initializer (the ``init`` method) is introduced to set-up a few
|
||||
internal variables: ``_start`` will hold the start of the timer (as
|
||||
|
@ -273,6 +254,184 @@ the button displayed in the UI. Then comes some actual logic:
|
|||
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 defaut, 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
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
# HG changeset patch
|
||||
# Parent 0000000000000000000000000000000000000000
|
||||
|
||||
diff --git a/__init__.py b/__init__.py
|
||||
new file mode 100644
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
new file mode 100644
|
||||
--- /dev/null
|
||||
+++ b/__openerp__.py
|
||||
@@ -0,0 +1,7 @@
|
||||
+# __openerp__.py
|
||||
+{
|
||||
+ 'name': "Web Example",
|
||||
+ 'description': "Basic example of a (future) web module",
|
||||
+ 'category': 'Hidden',
|
||||
+ 'depends': ['base'],
|
||||
+}
|
|
@ -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%;
|
||||
+}
|
|
@ -0,0 +1,11 @@
|
|||
# HG changeset patch
|
||||
# Parent 3ed382d9a8fe64fbb8e2bf4045e3fcd5c74c92bc
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
--- a/__openerp__.py
|
||||
+++ b/__openerp__.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'],
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
# HG changeset patch
|
||||
# Parent 43f21611dacb7c2b2f3810baeeef359ad7c329f0
|
||||
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
--- a/__openerp__.py
|
||||
+++ b/__openerp__.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>
|
|
@ -1,15 +1,17 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,11 +1,7 @@
|
||||
// static/src/js/first_module.js
|
||||
# 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({
|
||||
+ template: 'web_example.action'
|
||||
- className: 'oe_web_example',
|
||||
- start: function () {
|
||||
- this.$el.text("Hello, world!");
|
||||
- return this._super();
|
||||
- }
|
||||
+ template: 'web_example.action'
|
||||
});
|
||||
};
|
|
@ -1,7 +1,9 @@
|
|||
--- web_example/static/src/css/web_example.css
|
||||
+++ web_example/static/src/css/web_example.css
|
||||
@@ -1,6 +1,13 @@
|
||||
.openerp .oe_web_example {
|
||||
# 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%;
|
|
@ -1,7 +1,9 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,7 +1,19 @@
|
||||
// static/src/js/first_module.js
|
||||
# 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({
|
|
@ -1,12 +1,9 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
@@ -1,19 +1,52 @@
|
||||
// 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 = instance.web.Widget.extend({
|
||||
template: 'web_example.action',
|
||||
events: {
|
||||
# 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'
|
||||
},
|
|
@ -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")
|
||||
+ }
|
|
@ -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) {
|
|
@ -0,0 +1,12 @@
|
|||
# HG changeset patch
|
||||
# Parent 8a986919a3e22cd7cca51210820c09d4545dc60d
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
--- a/__openerp__.py
|
||||
+++ b/__openerp__.py
|
||||
@@ -3,5 +3,5 @@
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
- 'depends': ['base'],
|
||||
+ 'depends': ['web'],
|
||||
}
|
|
@ -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>
|
|
@ -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) {
|
|
@ -0,0 +1,6 @@
|
|||
Index: web_example/static/src/tests/timer.js
|
||||
===================================================================
|
||||
--- /dev/null
|
||||
+++ web_example/static/src/tests/timer.js
|
||||
@@ -0,0 +1 @@
|
||||
+
|
|
@ -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");
|
||||
+ });
|
||||
+});
|
|
@ -0,0 +1,10 @@
|
|||
Index: web_example/__openerp__.py
|
||||
===================================================================
|
||||
--- web_example.orig/__openerp__.py
|
||||
+++ web_example/__openerp__.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'],
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
+ });
|
||||
});
|
|
@ -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');
|
||||
+ });
|
||||
+ });
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
},
|
|
@ -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");
|
||||
+ });
|
||||
+ });
|
||||
});
|
|
@ -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");
|
|
@ -0,0 +1,11 @@
|
|||
# HG changeset patch
|
||||
# Parent 139dae60de67efa0017f5032f71ab774685c5507
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
--- a/__openerp__.py
|
||||
+++ b/__openerp__.py
|
||||
@@ -4,4 +4,5 @@
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
+ 'js': ['static/src/js/first_module.js'],
|
||||
}
|
|
@ -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");
|
||||
+};
|
|
@ -0,0 +1,29 @@
|
|||
# HG changeset patch
|
||||
# Parent 0026cb80097a724db8d36371bc00da993a51a06f
|
||||
|
||||
diff --git a/__openerp__.py b/__openerp__.py
|
||||
--- a/__openerp__.py
|
||||
+++ b/__openerp__.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>
|
|
@ -1,5 +1,8 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
# 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) {
|
|
@ -1,5 +1,8 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/static/src/js/first_module.js
|
||||
# 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) {
|
|
@ -1,7 +0,0 @@
|
|||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['base'],
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,7 +1,7 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
- 'depends': ['base'],
|
||||
+ 'depends': ['web'],
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,7 +1,8 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
+ 'js': ['static/src/js/first_module.js'],
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,8 +1,9 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
+ 'data': ['web_example.xml'],
|
||||
'js': ['static/src/js/first_module.js'],
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,9 +1,10 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'data': ['web_example.xml'],
|
||||
'js': ['static/src/js/first_module.js'],
|
||||
+ 'css': ['static/src/css/web_example.css'],
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
--- web_example/__openerp__.py
|
||||
+++ web_example/__openerp__.py
|
||||
@@ -1,10 +1,11 @@
|
||||
# __openerp__.py
|
||||
{
|
||||
'name': "Web Example",
|
||||
'description': "Basic example of a (future) web module",
|
||||
'category': 'Hidden',
|
||||
'depends': ['web'],
|
||||
'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'],
|
||||
}
|
|
@ -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
|
|
@ -1,6 +0,0 @@
|
|||
.openerp .oe_web_example {
|
||||
color: white;
|
||||
background-color: black;
|
||||
height: 100%;
|
||||
font-size: 400%;
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
// static/src/js/first_module.js
|
||||
console.log("Debug statement: file loaded");
|
|
@ -1,8 +0,0 @@
|
|||
--- web_example/static/src/js/first_module.js
|
||||
+++ web_example/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");
|
||||
+};
|
|
@ -1,11 +0,0 @@
|
|||
<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>
|
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
|
@ -1,11 +0,0 @@
|
|||
<!-- 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>
|
|
@ -107,6 +107,12 @@ 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
|
||||
+++++++++++++++++++++++++++++++++
|
||||
|
||||
|
@ -145,6 +151,11 @@ started only once (per view).
|
|||
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
|
||||
+++++++++++++++++++++++++++++
|
||||
|
||||
|
|
|
@ -329,6 +329,8 @@ a test case (or its containing test suite) through
|
|||
:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
|
||||
``rpc``.
|
||||
|
||||
.. _testing-rpc-mock:
|
||||
|
||||
Mock RPC
|
||||
++++++++
|
||||
|
||||
|
|
|
@ -93,7 +93,7 @@ The DOM root can also be defined programmatically by overridding
|
|||
Any override to :js:func:`~openerp.web.Widget.renderElement` which
|
||||
does not call its ``_super`` **must** call
|
||||
:js:func:`~openerp.web.Widget.setElement` with whatever it
|
||||
generated or the widget's behavior is undefined.r
|
||||
generated or the widget's behavior is undefined.
|
||||
|
||||
.. note::
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import time
|
|||
import traceback
|
||||
import urlparse
|
||||
import uuid
|
||||
import errno
|
||||
|
||||
import babel.core
|
||||
import simplejson
|
||||
|
@ -92,6 +93,14 @@ class WebRequest(object):
|
|||
if not self.session:
|
||||
self.session = session.OpenERPSession()
|
||||
self.httpsession[self.session_id] = self.session
|
||||
|
||||
# set db/uid trackers - they're cleaned up at the WSGI
|
||||
# dispatching phase in openerp.service.wsgi_server.application
|
||||
if self.session._db:
|
||||
threading.current_thread().dbname = self.session._db
|
||||
if self.session._uid:
|
||||
threading.current_thread().uid = self.session._uid
|
||||
|
||||
self.context = self.params.pop('context', {})
|
||||
self.debug = self.params.pop('debug', False) is not False
|
||||
# Determine self.lang
|
||||
|
@ -192,7 +201,7 @@ class JsonRequest(WebRequest):
|
|||
_logger.debug("--> %s.%s\n%s", method.im_class.__name__, method.__name__, pprint.pformat(self.jsonrequest))
|
||||
response['id'] = self.jsonrequest.get('id')
|
||||
response["result"] = method(self, **self.params)
|
||||
except session.AuthenticationError:
|
||||
except session.AuthenticationError, e:
|
||||
se = serialize_exception(e)
|
||||
error = {
|
||||
'code': 100,
|
||||
|
@ -346,17 +355,31 @@ def httprequest(f):
|
|||
addons_module = {}
|
||||
addons_manifest = {}
|
||||
controllers_class = []
|
||||
controllers_class_path = {}
|
||||
controllers_object = {}
|
||||
controllers_object_path = {}
|
||||
controllers_path = {}
|
||||
|
||||
class ControllerType(type):
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(ControllerType, cls).__init__(name, bases, attrs)
|
||||
controllers_class.append(("%s.%s" % (cls.__module__, cls.__name__), cls))
|
||||
name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls)
|
||||
controllers_class.append(name_class)
|
||||
path = attrs.get('_cp_path')
|
||||
if path not in controllers_class_path:
|
||||
controllers_class_path[path] = name_class
|
||||
|
||||
class Controller(object):
|
||||
__metaclass__ = ControllerType
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
subclasses = [c for c in cls.__subclasses__() if c._cp_path == cls._cp_path]
|
||||
if subclasses:
|
||||
name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses))
|
||||
cls = type(name, tuple(reversed(subclasses)), {})
|
||||
|
||||
return object.__new__(cls)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Session context manager
|
||||
#----------------------------------------------------------
|
||||
|
@ -468,8 +491,15 @@ def session_path():
|
|||
except Exception:
|
||||
username = "unknown"
|
||||
path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username)
|
||||
if not os.path.exists(path):
|
||||
try:
|
||||
os.mkdir(path, 0700)
|
||||
except OSError as exc:
|
||||
if exc.errno == errno.EEXIST:
|
||||
# directory exists: ensure it has the correct permissions
|
||||
# this will fail if the directory is not owned by the current user
|
||||
os.chmod(path, 0700)
|
||||
else:
|
||||
raise
|
||||
return path
|
||||
|
||||
class Root(object):
|
||||
|
@ -549,10 +579,11 @@ class Root(object):
|
|||
addons_manifest[module] = manifest
|
||||
self.statics['/%s/static' % module] = path_static
|
||||
|
||||
for k, v in controllers_class:
|
||||
if k not in controllers_object:
|
||||
o = v()
|
||||
controllers_object[k] = o
|
||||
for k, v in controllers_class_path.items():
|
||||
if k not in controllers_object_path and hasattr(v[1], '_cp_path'):
|
||||
o = v[1]()
|
||||
controllers_object[v[0]] = o
|
||||
controllers_object_path[k] = o
|
||||
if hasattr(o, '_cp_path'):
|
||||
controllers_path[o._cp_path] = o
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue