[IMP] missing sections to web module tutorial
bzr revid: xmo@openerp.com-20130418102250-aqt0qfhoff22xgj1
This commit is contained in:
commit
356be95d0c
|
@ -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>
|
|
@ -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::
|
||||
|
||||
|
|
Loading…
Reference in New Issue