[Merge]Merge with trunk

bzr revid: vja@tinyerp.com-20130501054129-2ie8kj73yosxbfo8
This commit is contained in:
Vishmita Jadeja (openerp) 2013-05-01 11:11:29 +05:30
commit 3b7863b479
400 changed files with 32959 additions and 27277 deletions

View File

@ -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",

View File

@ -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 = {

View File

@ -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']

View File

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

17
addons/web/doc/module/0 Normal file
View File

@ -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'],
+}

13
addons/web/doc/module/10 Normal file
View File

@ -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%;
+}

11
addons/web/doc/module/11 Normal file
View File

@ -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'],
}

28
addons/web/doc/module/12 Normal file
View File

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

View File

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

View File

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

View File

@ -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({

View File

@ -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'
},

19
addons/web/doc/module/18 Normal file
View File

@ -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")
+ }

52
addons/web/doc/module/19 Normal file
View File

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

12
addons/web/doc/module/2 Normal file
View File

@ -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'],
}

64
addons/web/doc/module/20 Normal file
View File

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

27
addons/web/doc/module/21 Normal file
View File

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

6
addons/web/doc/module/22 Normal file
View File

@ -0,0 +1,6 @@
Index: web_example/static/src/tests/timer.js
===================================================================
--- /dev/null
+++ web_example/static/src/tests/timer.js
@@ -0,0 +1 @@
+

14
addons/web/doc/module/23 Normal file
View File

@ -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");
+ });
+});

10
addons/web/doc/module/24 Normal file
View File

@ -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'],
}

55
addons/web/doc/module/25 Normal file
View File

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

38
addons/web/doc/module/26 Normal file
View File

@ -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');
+ });
});

28
addons/web/doc/module/27 Normal file
View File

@ -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');
+ });
+ });
});

13
addons/web/doc/module/28 Normal file
View File

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

37
addons/web/doc/module/29 Normal file
View File

@ -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");
+ });
+ });
});

9
addons/web/doc/module/3 Normal file
View File

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

11
addons/web/doc/module/4 Normal file
View File

@ -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'],
}

11
addons/web/doc/module/5 Normal file
View File

@ -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");
+};

29
addons/web/doc/module/6 Normal file
View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
# __openerp__.py
{
'name': "Web Example",
'description': "Basic example of a (future) web module",
'category': 'Hidden',
'depends': ['base'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

@ -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'],
}

View File

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

View File

@ -1,6 +0,0 @@
.openerp .oe_web_example {
color: white;
background-color: black;
height: 100%;
font-size: 400%;
}

View File

@ -1,2 +0,0 @@
// static/src/js/first_module.js
console.log("Debug statement: file loaded");

View File

@ -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");
+};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

2602
addons/web/i18n/lv.po Normal file

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