[MERGE] Merge with trunk upto revision no 1091.
bzr revid: ysa@tinyerp.com-20110926104619-uztxcf8278blbn7s
This commit is contained in:
@@ -2,27 +2,26 @@ import common
|
||||
import controllers
|
||||
import common.dispatch
|
||||
import logging
|
||||
import optparse
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class Options(object):
|
||||
pass
|
||||
|
||||
def wsgi_postload():
|
||||
import openerp.wsgi
|
||||
import openerp.tools
|
||||
import os
|
||||
import tempfile
|
||||
_logger.info("embedded mode")
|
||||
class Options(object):
|
||||
pass
|
||||
o = Options()
|
||||
o.dbfilter = '.*'
|
||||
o.dbfilter = openerp.tools.config['dbfilter']
|
||||
o.session_storage = os.path.join(tempfile.gettempdir(), "oe-sessions")
|
||||
o.addons_path = os.path.dirname(os.path.dirname(__file__))
|
||||
o.serve_static = True
|
||||
o.backend = 'local'
|
||||
|
||||
app = common.dispatch.Root(o)
|
||||
#import openerp.wsgi
|
||||
openerp.wsgi.register_wsgi_handler(app)
|
||||
|
||||
# TODO
|
||||
# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"static/lib/jquery.ui/js/jquery-ui-1.8.9.custom.min.js",
|
||||
"static/lib/jquery.ui/js/jquery-ui-timepicker-addon.js",
|
||||
"static/lib/jquery.ui.notify/js/jquery.notify.js",
|
||||
"static/lib/jquery.deferred-queue/jquery.deferred-queue.js",
|
||||
"static/lib/json/json2.js",
|
||||
"static/lib/qweb/qweb2.js",
|
||||
"static/lib/underscore/underscore.js",
|
||||
@@ -32,6 +33,7 @@
|
||||
"static/src/js/views.js",
|
||||
"static/src/js/data.js",
|
||||
"static/src/js/data_export.js",
|
||||
"static/src/js/data_import.js",
|
||||
"static/src/js/search.js",
|
||||
"static/src/js/view_form.js",
|
||||
"static/src/js/view_list.js",
|
||||
@@ -44,6 +46,7 @@
|
||||
"static/lib/jquery.ui.notify/css/ui.notify.css",
|
||||
"static/src/css/base.css",
|
||||
"static/src/css/data_export.css",
|
||||
"static/src/css/data_import.css",
|
||||
],
|
||||
'post_load' : 'wsgi_postload',
|
||||
}
|
||||
|
||||
@@ -21,8 +21,7 @@ import werkzeug.wsgi
|
||||
import ast
|
||||
import nonliterals
|
||||
import http
|
||||
# import backendlocal as backend
|
||||
import session as backend
|
||||
import session
|
||||
import openerplib
|
||||
|
||||
__all__ = ['Root', 'jsonrequest', 'httprequest', 'Controller',
|
||||
@@ -79,12 +78,12 @@ class WebRequest(object):
|
||||
|
||||
.. attribute:: session_id
|
||||
|
||||
opaque identifier for the :class:`backend.OpenERPSession` instance of
|
||||
opaque identifier for the :class:`session.OpenERPSession` instance of
|
||||
the current request
|
||||
|
||||
.. attribute:: session
|
||||
|
||||
:class:`~backend.OpenERPSession` instance for the current request
|
||||
:class:`~session.OpenERPSession` instance for the current request
|
||||
|
||||
.. attribute:: context
|
||||
|
||||
@@ -100,13 +99,12 @@ class WebRequest(object):
|
||||
self.httpresponse = None
|
||||
self.httpsession = request.session
|
||||
self.config = config
|
||||
|
||||
def init(self, params):
|
||||
self.params = dict(params)
|
||||
# OpenERP session setup
|
||||
self.session_id = self.params.pop("session_id", None) or uuid.uuid4().hex
|
||||
self.session = self.httpsession.setdefault(
|
||||
self.session_id, backend.OpenERPSession(
|
||||
self.config.server_host, self.config.server_port))
|
||||
self.session = self.httpsession.setdefault(self.session_id, session.OpenERPSession(self.config))
|
||||
self.context = self.params.pop('context', None)
|
||||
self.debug = self.params.pop('debug', False) != False
|
||||
|
||||
@@ -389,15 +387,14 @@ class Root(object):
|
||||
for module in os.listdir(addons_path):
|
||||
if module not in addons_module:
|
||||
manifest_path = os.path.join(addons_path, module, '__openerp__.py')
|
||||
if os.path.isfile(manifest_path):
|
||||
path_static = os.path.join(addons_path, module, 'static')
|
||||
if os.path.isfile(manifest_path) and os.path.isdir(path_static):
|
||||
manifest = ast.literal_eval(open(manifest_path).read())
|
||||
_logger.info("Loading %s", module)
|
||||
m = __import__(module)
|
||||
addons_module[module] = m
|
||||
addons_manifest[module] = manifest
|
||||
|
||||
statics['/%s/static' % module] = \
|
||||
os.path.join(addons_path, module, 'static')
|
||||
statics['/%s/static' % module] = path_static
|
||||
for k, v in controllers_class.items():
|
||||
if k not in controllers_object:
|
||||
o = v()
|
||||
|
||||
@@ -190,6 +190,34 @@ class NetRPCConnector(Connector):
|
||||
socket.disconnect()
|
||||
return result
|
||||
|
||||
class LocalConnector(Connector):
|
||||
"""
|
||||
A type of connector that uses the XMLRPC protocol.
|
||||
"""
|
||||
PROTOCOL = 'local'
|
||||
|
||||
__logger = _getChildLogger(_logger, 'connector.local')
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def send(self, service_name, method, *args):
|
||||
import openerp
|
||||
# TODO Exception handling
|
||||
# This will be changed to be xmlrpc compatible
|
||||
# OpenERPWarning code 1
|
||||
# OpenERPException code 2
|
||||
try:
|
||||
result = openerp.netsvc.dispatch_rpc(service_name, method, args, None)
|
||||
except openerp.netsvc.OpenERPDispatcherException, e:
|
||||
fault = xmlrpclib.Fault(openerp.tools.exception_to_unicode(e.exception), e.traceback)
|
||||
raise fault
|
||||
except:
|
||||
exc_type, exc_value, exc_tb = sys.exc_info()
|
||||
fault = xmlrpclib.Fault(1, "%s:%s" % (exc_type, exc_value))
|
||||
raise fault
|
||||
return result
|
||||
|
||||
class Service(object):
|
||||
"""
|
||||
A class to execute RPC calls on a specific service of the remote server.
|
||||
@@ -363,7 +391,7 @@ class Model(object):
|
||||
records = self.read(record_ids, fields or [], context or {})
|
||||
return records
|
||||
|
||||
def get_connector(hostname, protocol="xmlrpc", port="auto"):
|
||||
def get_connector(hostname=None, protocol="xmlrpc", port="auto"):
|
||||
"""
|
||||
A shortcut method to easily create a connector to a remote server using XMLRPC or NetRPC.
|
||||
|
||||
@@ -377,10 +405,12 @@ def get_connector(hostname, protocol="xmlrpc", port="auto"):
|
||||
return XmlRPCConnector(hostname, port)
|
||||
elif protocol == "netrpc":
|
||||
return NetRPCConnector(hostname, port)
|
||||
elif protocol == "local":
|
||||
return LocalConnector()
|
||||
else:
|
||||
raise ValueError("You must choose xmlrpc or netrpc")
|
||||
raise ValueError("You must choose xmlrpc or netrpc or local")
|
||||
|
||||
def get_connection(hostname, protocol="xmlrpc", port='auto', database=None,
|
||||
def get_connection(hostname=None, protocol="xmlrpc", port='auto', database=None,
|
||||
login=None, password=None, user_id=None):
|
||||
"""
|
||||
A shortcut method to easily create a connection to a remote OpenERP server.
|
||||
|
||||
@@ -26,9 +26,8 @@ class OpenERPSession(object):
|
||||
Used to store references to non-literal domains which need to be
|
||||
round-tripped to the client browser.
|
||||
"""
|
||||
def __init__(self, server='127.0.0.1', port=8069):
|
||||
self._server = server
|
||||
self._port = port
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self._db = False
|
||||
self._uid = False
|
||||
self._login = False
|
||||
@@ -40,11 +39,16 @@ class OpenERPSession(object):
|
||||
self._lang = {}
|
||||
self.remote_timezone = 'utc'
|
||||
self.client_timezone = False
|
||||
|
||||
|
||||
def build_connection(self):
|
||||
return openerplib.get_connection(hostname=self._server, port=self._port,
|
||||
database=self._db, login=self._login,
|
||||
user_id=self._uid, password=self._password)
|
||||
if self.config.backend == 'local':
|
||||
conn = openerplib.get_connection(protocol='local', database=self._db,
|
||||
login=self._login, user_id=self._uid, password=self._password)
|
||||
else:
|
||||
conn = openerplib.get_connection(hostname=self.config.server_host,
|
||||
port=self.config.server_port, database=self._db, login=self._login,
|
||||
user_id=self._uid, password=self._password)
|
||||
return conn
|
||||
|
||||
def proxy(self, service):
|
||||
return self.build_connection().get_service(service)
|
||||
|
||||
@@ -166,14 +166,14 @@ class WebClient(openerpweb.Controller):
|
||||
"grouping", "decimal_point", "thousands_sep"])
|
||||
else:
|
||||
lang_obj = None
|
||||
|
||||
|
||||
if lang.count("_") > 0:
|
||||
separator = "_"
|
||||
else:
|
||||
separator = "@"
|
||||
langs = lang.split(separator)
|
||||
langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
|
||||
|
||||
|
||||
transs = {}
|
||||
for addon_name in mods:
|
||||
transl = {"messages":[]}
|
||||
@@ -240,7 +240,7 @@ class Database(openerpweb.Controller):
|
||||
password, db = operator.itemgetter(
|
||||
'drop_pwd', 'drop_db')(
|
||||
dict(map(operator.itemgetter('name', 'value'), fields)))
|
||||
|
||||
|
||||
try:
|
||||
return req.session.proxy("db").drop(password, db)
|
||||
except xmlrpclib.Fault, e:
|
||||
@@ -336,7 +336,7 @@ class Session(openerpweb.Controller):
|
||||
}
|
||||
except Exception, e:
|
||||
return {"error": e, "title": "Languages"}
|
||||
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def modules(self, req):
|
||||
# TODO query server for installed web modules
|
||||
@@ -715,7 +715,7 @@ class DataSet(openerpweb.Controller):
|
||||
args[domain_id] = d
|
||||
if context_id and len(args) - 1 >= context_id:
|
||||
args[context_id] = c
|
||||
|
||||
|
||||
for i in xrange(len(args)):
|
||||
if isinstance(args[i], web.common.nonliterals.BaseContext):
|
||||
args[i] = req.session.eval_context(args[i])
|
||||
@@ -963,7 +963,7 @@ class SearchView(View):
|
||||
if field.get('context'):
|
||||
field["context"] = self.parse_domain(field["context"], req.session)
|
||||
return {'fields': fields}
|
||||
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def get_filters(self, req, model):
|
||||
Model = req.session.model("ir.filters")
|
||||
@@ -972,7 +972,7 @@ class SearchView(View):
|
||||
filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
|
||||
filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
|
||||
return filters
|
||||
|
||||
|
||||
@openerpweb.jsonrequest
|
||||
def save_filter(self, req, model, name, context_to_save, domain):
|
||||
Model = req.session.model("ir.filters")
|
||||
@@ -1382,7 +1382,7 @@ class Reports(View):
|
||||
report_data['form'] = action['datas']['form']
|
||||
if 'ids' in action['datas']:
|
||||
report_ids = action['datas']['ids']
|
||||
|
||||
|
||||
report_id = report_srv.report(
|
||||
req.session._db, req.session._uid, req.session._password,
|
||||
action["report_name"], report_ids,
|
||||
@@ -1394,6 +1394,7 @@ class Reports(View):
|
||||
req.session._db, req.session._uid, req.session._password, report_id)
|
||||
if report_struct["state"]:
|
||||
break
|
||||
|
||||
time.sleep(self.POLLING_DELAY)
|
||||
|
||||
report = base64.b64decode(report_struct['result'])
|
||||
@@ -1407,3 +1408,108 @@ class Reports(View):
|
||||
('Content-Type', report_mimetype),
|
||||
('Content-Length', len(report))],
|
||||
cookies={'fileToken': int(token)})
|
||||
|
||||
|
||||
class Import(View):
|
||||
_cp_path = "/web/import"
|
||||
|
||||
def fields_get(self, req, model):
|
||||
Model = req.session.model(model)
|
||||
fields = Model.fields_get(False, req.session.eval_context(req.context))
|
||||
return fields
|
||||
|
||||
@openerpweb.httprequest
|
||||
def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp):
|
||||
try:
|
||||
data = list(csv.reader(
|
||||
csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
|
||||
except csv.Error, e:
|
||||
csvfile.seek(0)
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'error': {
|
||||
'message': 'Error parsing CSV file: %s' % e,
|
||||
# decodes each byte to a unicode character, which may or
|
||||
# may not be printable, but decoding will succeed.
|
||||
# Otherwise simplejson will try to decode the `str` using
|
||||
# utf-8, which is very likely to blow up on characters out
|
||||
# of the ascii range (in range [128, 256))
|
||||
'preview': csvfile.read(200).decode('iso-8859-1')}}))
|
||||
|
||||
try:
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps(
|
||||
{'records': data[:10]}, encoding=csvcode))
|
||||
except UnicodeDecodeError:
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({
|
||||
'message': u"Failed to decode CSV file using encoding %s, "
|
||||
u"try switching to a different encoding" % csvcode
|
||||
}))
|
||||
|
||||
@openerpweb.httprequest
|
||||
def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
|
||||
meta):
|
||||
modle_obj = req.session.model(model)
|
||||
skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
|
||||
simplejson.loads(meta))
|
||||
|
||||
error = None
|
||||
if not (csvdel and len(csvdel) == 1):
|
||||
error = u"The CSV delimiter must be a single character"
|
||||
|
||||
if not indices and fields:
|
||||
error = u"You must select at least one field to import"
|
||||
|
||||
if error:
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'error': {'message': error}}))
|
||||
|
||||
# skip ignored records
|
||||
data_record = itertools.islice(
|
||||
csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
|
||||
skip, None)
|
||||
|
||||
# if only one index, itemgetter will return an atom rather than a tuple
|
||||
if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
|
||||
else: mapper = operator.itemgetter(*indices)
|
||||
|
||||
data = None
|
||||
error = None
|
||||
try:
|
||||
# decode each data row
|
||||
data = [
|
||||
[record.decode(csvcode) for record in row]
|
||||
for row in itertools.imap(mapper, data_record)
|
||||
# don't insert completely empty rows (can happen due to fields
|
||||
# filtering in case of e.g. o2m content rows)
|
||||
if any(row)
|
||||
]
|
||||
except UnicodeDecodeError:
|
||||
error = u"Failed to decode CSV file using encoding %s" % csvcode
|
||||
except csv.Error, e:
|
||||
error = u"Could not process CSV file: %s" % e
|
||||
|
||||
# If the file contains nothing,
|
||||
if not data:
|
||||
error = u"File to import is empty"
|
||||
if error:
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'error': {'message': error}}))
|
||||
|
||||
try:
|
||||
(code, record, message, _nope) = modle_obj.import_data(
|
||||
fields, data, 'init', '', False,
|
||||
req.session.eval_context(req.context))
|
||||
except xmlrpclib.Fault, e:
|
||||
error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'error':error}))
|
||||
|
||||
if code != -1:
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'success':True}))
|
||||
|
||||
msg = u"Error during import: %s\n\nTrying to import record %r" % (
|
||||
message, record)
|
||||
return '<script>window.top.%s(%s);</script>' % (
|
||||
jsonp, simplejson.dumps({'error': {'message':msg}}))
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
Copyright 2011 Xavier Morel. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification, are
|
||||
permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this list of
|
||||
conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice, this list
|
||||
of conditions and the following disclaimer in the documentation and/or other materials
|
||||
provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY XAVIER MOREL ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> OR
|
||||
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,59 @@
|
||||
.. -*- restructuredtext -*-
|
||||
|
||||
In jQuery 1.5, jQuery has introduced a Deferred object in order to
|
||||
better handle callbacks.
|
||||
|
||||
Along with Deferred, it introduced ``jQuery.when`` which allows, among
|
||||
other things, for multiplexing deferreds (waiting on multiple
|
||||
deferreds at the same time).
|
||||
|
||||
While this is very nice if all deferreds are available at the same
|
||||
point, it doesn't really work if the resolution of a deferred can
|
||||
generate more deferreds, or for collections of deferreds coming from
|
||||
multiple sources (which may not be aware of one another).
|
||||
|
||||
Deferred.queue tries to be a solution to this. It is based on the
|
||||
principle of FIFO multiple-producers multiple-consumers tasks queues,
|
||||
such as Python's `queue.Queue`_. Any code with a reference to the
|
||||
queue can add a promise to the queue, and the queue (which is a
|
||||
promise itself) will only be resolved once all promises within are
|
||||
resolved.
|
||||
|
||||
Quickstart
|
||||
----------
|
||||
|
||||
Deferred.queue has a very simple life cycle: it is dormant when
|
||||
created, it starts working as soon as promises get added to it (via
|
||||
``.push``, or by providing them directly to its constructor), and as
|
||||
soon as the last promise in the queue is resolved it resolves itself.
|
||||
|
||||
If any promise in the queue fails, the queue itself will be rejected
|
||||
(without waiting for further promises).
|
||||
|
||||
Once a queue has been resolved or rejected, adding new promises to it
|
||||
results in an error.
|
||||
|
||||
API
|
||||
---
|
||||
|
||||
``jQuery.Deferred.queue([promises...])``
|
||||
Creates a new deferred queue. Can be primed with a series of promise
|
||||
objects.
|
||||
|
||||
``.push(promise...)``
|
||||
Adds promises to the queue. Returns the queue itself (can be
|
||||
chained).
|
||||
|
||||
If the queue has already been resolved or rejected, raises an error.
|
||||
|
||||
``.then([doneCallbacks][, failCallbacks])``
|
||||
Promise/A ``then`` method.
|
||||
|
||||
``.done(doneCallbacks)``
|
||||
jQuery ``done`` extension to promise objects
|
||||
|
||||
``.fail(failCallbacks)``
|
||||
jQuery ``fail`` extension to promise objects
|
||||
|
||||
.. _queue.Queue:
|
||||
http://docs.python.org/dev/library/queue.html
|
||||
@@ -0,0 +1,34 @@
|
||||
(function ($) {
|
||||
"use strict";
|
||||
$.extend($.Deferred, {
|
||||
queue: function () {
|
||||
var queueDeferred = $.Deferred();
|
||||
var promises = 0;
|
||||
|
||||
function resolve() {
|
||||
if (--promises > 0) {
|
||||
return;
|
||||
}
|
||||
setTimeout($.proxy(queueDeferred, 'resolve'), 0);
|
||||
}
|
||||
|
||||
var promise = $.extend(queueDeferred.promise(), {
|
||||
push: function () {
|
||||
if (this.isResolved() || this.isRejected()) {
|
||||
throw new Error("Can not add promises to a resolved "
|
||||
+ "or rejected promise queue");
|
||||
}
|
||||
|
||||
promises += 1;
|
||||
$.when.apply(null, arguments).then(
|
||||
resolve, $.proxy(queueDeferred, 'reject'));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
if (arguments.length) {
|
||||
promise.push.apply(promise, arguments);
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
});
|
||||
})(jQuery)
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
.openerp .oe_import_grid {
|
||||
border: none;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.openerp .oe_import_grid-header .oe_import_grid-cell {
|
||||
background: url(../img/gradientlinebg.gif) repeat-x #CCCCCC;
|
||||
border-bottom: 1px solid #E3E3E3;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
}
|
||||
.openerp .oe_import_grid-row .oe_import_grid-cell {
|
||||
border-bottom: 1px solid #E3E3E3;
|
||||
}
|
||||
.openerp .separator.horizontal {
|
||||
font-weight: bold;
|
||||
border-bottom-width: 1px;
|
||||
margin: 6px 4px 6px 1px;
|
||||
height: 20px;
|
||||
}
|
||||
.openerp .duplicate_fld{
|
||||
background-color:#FF6666;
|
||||
}
|
||||
.openerp .select_fld{
|
||||
background: none repeat scroll 0 0 white;
|
||||
}
|
||||
.openerp .ui-autocomplete {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
padding-right: 20px;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 182 B |
@@ -58,7 +58,7 @@ openerp.web = function(instance) {
|
||||
openerp.web.formats(instance);
|
||||
openerp.web.chrome(instance);
|
||||
openerp.web.data(instance);
|
||||
var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export"];
|
||||
var files = ["views","search","list","form","list_editable","web_mobile","view_tree","data_export","data_import"];
|
||||
for(var i=0; i<files.length; i++) {
|
||||
if(openerp.web[files[i]]) {
|
||||
openerp.web[files[i]](instance);
|
||||
|
||||
@@ -218,9 +218,8 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
|
||||
this.$element.closest(".openerp")
|
||||
.removeClass("login-mode")
|
||||
.addClass("database_block");
|
||||
|
||||
|
||||
var self = this;
|
||||
|
||||
var fetch_db = this.rpc("/web/database/get_list", {}, function(result) {
|
||||
self.db_list = result.db_list;
|
||||
});
|
||||
@@ -232,7 +231,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
|
||||
self.lang_list = result.lang_list;
|
||||
});
|
||||
$.when(fetch_db, fetch_langs).then(function () {self.do_create();});
|
||||
|
||||
|
||||
this.$element.find('#db-create').click(this.do_create);
|
||||
this.$element.find('#db-drop').click(this.do_drop);
|
||||
this.$element.find('#db-backup').click(this.do_backup);
|
||||
@@ -399,7 +398,7 @@ openerp.web.Database = openerp.web.Widget.extend(/** @lends openerp.web.Database
|
||||
do_restore: function() {
|
||||
var self = this;
|
||||
self.$option_id.html(QWeb.render("RestoreDB", self));
|
||||
|
||||
|
||||
self.$option_id.find("form[name=restore_db_form]").validate({
|
||||
submitHandler: function (form) {
|
||||
$.blockUI({message:'<img src="/web/static/src/img/throbber2.gif">'});
|
||||
@@ -467,10 +466,11 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
|
||||
/**
|
||||
* @constructs openerp.web.Login
|
||||
* @extends openerp.web.Widget
|
||||
*
|
||||
*
|
||||
* @param parent
|
||||
* @param element_id
|
||||
*/
|
||||
|
||||
init: function(parent, element_id) {
|
||||
this._super(parent, element_id);
|
||||
this.has_local_storage = typeof(localStorage) != 'undefined';
|
||||
@@ -496,7 +496,7 @@ openerp.web.Login = openerp.web.Widget.extend(/** @lends openerp.web.Login# */{
|
||||
},
|
||||
display: function() {
|
||||
var self = this;
|
||||
|
||||
|
||||
this.$element.html(QWeb.render("Login", this));
|
||||
this.database = new openerp.web.Database(
|
||||
this, "oe_database", "oe_db_options");
|
||||
@@ -574,7 +574,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
|
||||
/**
|
||||
* @constructs openerp.web.Header
|
||||
* @extends openerp.web.Widget
|
||||
*
|
||||
*
|
||||
* @param parent
|
||||
*/
|
||||
init: function(parent) {
|
||||
@@ -665,7 +665,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
on_action: function(action) {
|
||||
},
|
||||
on_preferences: function(){
|
||||
@@ -713,7 +713,7 @@ openerp.web.Header = openerp.web.Widget.extend(/** @lends openerp.web.Header# *
|
||||
action_manager.appendTo(this.dialog);
|
||||
action_manager.render(this.dialog);
|
||||
},
|
||||
|
||||
|
||||
change_password :function() {
|
||||
var self = this;
|
||||
this.dialog = new openerp.web.Dialog(this,{
|
||||
@@ -758,7 +758,7 @@ openerp.web.Menu = openerp.web.Widget.extend(/** @lends openerp.web.Menu# */{
|
||||
/**
|
||||
* @constructs openerp.web.Menu
|
||||
* @extends openerp.web.Widget
|
||||
*
|
||||
*
|
||||
* @param parent
|
||||
* @param element_id
|
||||
* @param secondary_menu_id
|
||||
@@ -910,7 +910,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
|
||||
/**
|
||||
* @constructs openerp.web.WebClient
|
||||
* @extends openerp.web.Widget
|
||||
*
|
||||
*
|
||||
* @param element_id
|
||||
*/
|
||||
init: function(element_id) {
|
||||
@@ -1000,7 +1000,7 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
|
||||
self.execute_home_action(home_action[0], ds);
|
||||
})
|
||||
},
|
||||
default_home: function () {
|
||||
default_home: function () {
|
||||
},
|
||||
/**
|
||||
* Bundles the execution of the home action
|
||||
@@ -1025,7 +1025,6 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
|
||||
},
|
||||
do_url_set_hash: function(url) {
|
||||
if(!this.url_external_hashchange) {
|
||||
console.log("url set #hash to",url);
|
||||
this.url_internal_hashchange = true;
|
||||
jQuery.bbq.pushState(url);
|
||||
}
|
||||
@@ -1033,10 +1032,8 @@ openerp.web.WebClient = openerp.web.Widget.extend(/** @lends openerp.web.WebClie
|
||||
on_url_hashchange: function() {
|
||||
if(this.url_internal_hashchange) {
|
||||
this.url_internal_hashchange = false;
|
||||
console.log("url jump to FLAG OFF");
|
||||
} else {
|
||||
var url = jQuery.deparam.fragment();
|
||||
console.log("url jump to",url);
|
||||
this.url_external_hashchange = true;
|
||||
this.action_manager.on_url_hashchange(url);
|
||||
this.url_external_hashchange = false;
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
openerp.web.data_import = function(openerp) {
|
||||
var QWeb = openerp.web.qweb;
|
||||
/**
|
||||
* Safari does not deal well at all with raw JSON data being returned. As a
|
||||
* result, we're going to cheat by using a pseudo-jsonp: instead of getting
|
||||
* JSON data in the iframe, we're getting a ``script`` tag which consists of a
|
||||
* function call and the returned data (the json dump).
|
||||
*
|
||||
* The function is an auto-generated name bound to ``window``, which calls
|
||||
* back into the callback provided here.
|
||||
*
|
||||
* @param {Object} form the form element (DOM or jQuery) to use in the call
|
||||
* @param {Object} attributes jquery.form attributes object
|
||||
* @param {Function} callback function to call with the returned data
|
||||
*/
|
||||
function jsonp(form, attributes, callback) {
|
||||
attributes = attributes || {};
|
||||
var options = {jsonp: _.uniqueId('import_callback_')};
|
||||
window[options.jsonp] = function () {
|
||||
delete window[options.jsonp];
|
||||
callback.apply(null, arguments);
|
||||
};
|
||||
if ('data' in attributes) {
|
||||
_.extend(attributes.data, options);
|
||||
} else {
|
||||
_.extend(attributes, {data: options});
|
||||
}
|
||||
$(form).ajaxSubmit(attributes);
|
||||
}
|
||||
|
||||
openerp.web.DataImport = openerp.web.Dialog.extend({
|
||||
template: 'ImportDataView',
|
||||
dialog_title: "Import Data",
|
||||
init: function(parent, dataset){
|
||||
var self = this;
|
||||
this._super(parent, {});
|
||||
this.model = parent.model;
|
||||
this.fields = [];
|
||||
this.all_fields = [];
|
||||
this.required_fields = null;
|
||||
|
||||
var convert_fields = function (root, prefix) {
|
||||
prefix = prefix || '';
|
||||
_(root.fields).each(function (f) {
|
||||
self.all_fields.push(prefix + f.name);
|
||||
if (f.fields) {
|
||||
convert_fields(f, prefix + f.id + '/');
|
||||
}
|
||||
});
|
||||
};
|
||||
this.ready = $.Deferred.queue().then(function () {
|
||||
self.required_fields = _(self.fields).chain()
|
||||
.filter(function (field) { return field.required; })
|
||||
.pluck('name')
|
||||
.value();
|
||||
convert_fields(self);
|
||||
self.all_fields.sort();
|
||||
});
|
||||
},
|
||||
start: function() {
|
||||
var self = this;
|
||||
this._super();
|
||||
this.open({
|
||||
modal: true,
|
||||
width: '70%',
|
||||
height: 'auto',
|
||||
position: 'top',
|
||||
buttons: [
|
||||
{text: "Close", click: function() { self.stop(); }},
|
||||
{text: "Import File", click: function() { self.do_import(); }, 'class': 'oe-dialog-import-button'}
|
||||
],
|
||||
close: function(event, ui) {
|
||||
self.stop();
|
||||
}
|
||||
});
|
||||
this.toggle_import_button(false);
|
||||
this.$element.find('#csvfile').change(this.on_autodetect_data);
|
||||
this.$element.find('fieldset').change(this.on_autodetect_data);
|
||||
this.$element.find('fieldset legend').click(function() {
|
||||
$(this).next().toggle();
|
||||
});
|
||||
this.ready.push(new openerp.web.DataSet(this, this.model).call(
|
||||
'fields_get', [], function (fields) {
|
||||
self.graft_fields(fields);
|
||||
}));
|
||||
},
|
||||
graft_fields: function (fields, parent, level) {
|
||||
parent = parent || this;
|
||||
level = level || 0;
|
||||
|
||||
var self = this;
|
||||
_(fields).each(function (field, field_name) {
|
||||
var f = {
|
||||
id: field_name,
|
||||
name: field_name,
|
||||
string: field.string,
|
||||
required: field.required
|
||||
};
|
||||
|
||||
switch (field.type) {
|
||||
case 'many2many':
|
||||
case 'many2one':
|
||||
f.name += '/id';
|
||||
break;
|
||||
case 'one2many':
|
||||
f.name += '/id';
|
||||
f.fields = [];
|
||||
// only fetch sub-fields to a depth of 2 levels
|
||||
if (level < 2) {
|
||||
self.ready.push(new openerp.web.DataSet(self, field.relation).call(
|
||||
'fields_get', [], function (fields) {
|
||||
self.graft_fields(fields, f, level+1);
|
||||
}));
|
||||
}
|
||||
break;
|
||||
}
|
||||
parent.fields.push(f);
|
||||
});
|
||||
},
|
||||
toggle_import_button: function (newstate) {
|
||||
this.$dialog.dialog('widget')
|
||||
.find('.oe-dialog-import-button')
|
||||
.button('option', 'disabled', !newstate);
|
||||
},
|
||||
do_import: function() {
|
||||
if(!this.$element.find('#csvfile').val()) { return; }
|
||||
var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10);
|
||||
var with_headers = this.$element.find('#file_has_headers').prop('checked');
|
||||
if (!lines_to_skip && with_headers) {
|
||||
lines_to_skip = 1;
|
||||
}
|
||||
var indices = [], fields = [];
|
||||
this.$element.find(".sel_fields").each(function(index, element) {
|
||||
var val = element.value;
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
indices.push(index);
|
||||
fields.push(val);
|
||||
});
|
||||
|
||||
jsonp(this.$element.find('#import_data'), {
|
||||
url: '/web/import/import_data',
|
||||
data: {
|
||||
model: this.model,
|
||||
meta: JSON.stringify({
|
||||
skip: lines_to_skip,
|
||||
indices: indices,
|
||||
fields: fields
|
||||
})
|
||||
}
|
||||
}, this.on_import_results);
|
||||
},
|
||||
on_autodetect_data: function() {
|
||||
if(!this.$element.find('#csvfile').val()) { return; }
|
||||
jsonp(this.$element.find('#import_data'), {
|
||||
url: '/web/import/detect_data'
|
||||
}, this.on_import_results);
|
||||
},
|
||||
on_import_results: function(results) {
|
||||
this.$element.find('#result').empty();
|
||||
var headers, result_node = this.$element.find("#result");
|
||||
|
||||
if (results['records']) {
|
||||
var lines_to_skip = parseInt(this.$element.find('#csv_skip').val(), 10),
|
||||
with_headers = this.$element.find('#file_has_headers').prop('checked');
|
||||
headers = with_headers ? results.records[0] : null;
|
||||
|
||||
result_node.append(QWeb.render('ImportView.result', {
|
||||
'headers': headers,
|
||||
'records': lines_to_skip ? results.records.slice(lines_to_skip)
|
||||
: with_headers ? results.records.slice(1)
|
||||
: results.records
|
||||
}));
|
||||
} else if (results['error']) {
|
||||
result_node.append(QWeb.render('ImportView.error', {
|
||||
'error': results['error']}));
|
||||
} else if (results['success']) {
|
||||
if (this.widget_parent.widget_parent.active_view == "list") {
|
||||
this.widget_parent.reload_content();
|
||||
}
|
||||
this.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
this.ready.then(function () {
|
||||
var $fields = self.$element.find('.sel_fields').bind('blur', function () {
|
||||
if (this.value && !_(self.all_fields).contains(this.value)) {
|
||||
this.value = '';
|
||||
}
|
||||
}).autocomplete({
|
||||
minLength: 0,
|
||||
source: self.all_fields,
|
||||
change: self.on_check_field_values
|
||||
}).focus(function () {
|
||||
$(this).autocomplete('search');
|
||||
});
|
||||
// Column auto-detection
|
||||
_(headers).each(function (header, index) {
|
||||
var f =_(self.fields).detect(function (field) {
|
||||
// TODO: levenshtein between header and field.string
|
||||
return field.name === header || field.string.toLowerCase() === header;
|
||||
});
|
||||
if (f) {
|
||||
$fields.eq(index).val(f.name);
|
||||
}
|
||||
});
|
||||
self.on_check_field_values();
|
||||
});
|
||||
},
|
||||
/**
|
||||
* Looks through all the field selections, and tries to find if two
|
||||
* (or more) columns were matched to the same model field.
|
||||
*
|
||||
* Returns a map of the multiply-mapped fields to an array of offending
|
||||
* columns (not actually columns, but the inputs containing the same field
|
||||
* names).
|
||||
*
|
||||
* Also has the side-effect of marking the discovered inputs with the class
|
||||
* ``duplicate_fld``.
|
||||
*
|
||||
* @returns {Object<String, Array<String>>} map of duplicate field matches to same-valued inputs
|
||||
*/
|
||||
find_duplicate_fields: function() {
|
||||
// Maps values to DOM nodes, in order to discover duplicates
|
||||
var values = {}, duplicates = {};
|
||||
this.$element.find(".sel_fields").each(function(index, element) {
|
||||
var value = element.value;
|
||||
var $element = $(element).removeClass('duplicate_fld');
|
||||
if (!value) { return; }
|
||||
|
||||
if (!(value in values)) {
|
||||
values[value] = element;
|
||||
} else {
|
||||
var same_valued_field = values[value];
|
||||
if (value in duplicates) {
|
||||
duplicates[value].push(element);
|
||||
} else {
|
||||
duplicates[value] = [same_valued_field, element];
|
||||
}
|
||||
$element.add(same_valued_field).addClass('duplicate_fld');
|
||||
}
|
||||
});
|
||||
return duplicates;
|
||||
},
|
||||
on_check_field_values: function () {
|
||||
this.$element.find("#message, #msg").remove();
|
||||
|
||||
var required_valid = this.check_required();
|
||||
|
||||
var duplicates = this.find_duplicate_fields();
|
||||
if (_.isEmpty(duplicates)) {
|
||||
this.toggle_import_button(required_valid);
|
||||
} else {
|
||||
var $err = $('<div id="msg" style="color: red;">Destination fields should only be selected once, some fields are selected more than once:</div>').insertBefore(this.$element.find('#result'));
|
||||
var $dupes = $('<dl>').appendTo($err);
|
||||
_(duplicates).each(function(elements, value) {
|
||||
$('<dt>').text(value).appendTo($dupes);
|
||||
_(elements).each(function(element) {
|
||||
var cell = $(element).closest('td');
|
||||
$('<dd>').text(cell.parent().children().index(cell)).appendTo($dupes);
|
||||
});
|
||||
});
|
||||
this.toggle_import_button(false);
|
||||
}
|
||||
|
||||
},
|
||||
check_required: function() {
|
||||
if (!this.required_fields.length) { return true; }
|
||||
|
||||
var selected_fields = _(this.$element.find('.sel_fields').get()).chain()
|
||||
.pluck('value')
|
||||
.compact()
|
||||
.value();
|
||||
|
||||
var missing_fields = _.difference(this.required_fields, selected_fields);
|
||||
if (missing_fields.length) {
|
||||
this.$element.find("#result").before('<div id="message" style="color:red">*Required Fields are not selected : ' + missing_fields + '.</div>');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
stop: function() {
|
||||
$(this.$dialog).remove();
|
||||
this._super();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -1467,7 +1467,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
|
||||
this.$input = this.$element.find("input");
|
||||
this.$drop_down = this.$element.find(".oe-m2o-drop-down-button");
|
||||
this.$menu_btn = this.$element.find(".oe-m2o-cm-button");
|
||||
|
||||
|
||||
// context menu
|
||||
var init_context_menu_def = $.Deferred().then(function(e) {
|
||||
var rdataset = new openerp.web.DataSetStatic(self, "ir.values", self.build_context());
|
||||
@@ -1475,7 +1475,7 @@ openerp.web.form.FieldMany2One = openerp.web.form.Field.extend({
|
||||
[[self.field.relation, false]], false, rdataset.get_context()], false, 0)
|
||||
.then(function(result) {
|
||||
self.related_entries = result;
|
||||
|
||||
|
||||
var $cmenu = $("#" + self.cm_id);
|
||||
$cmenu.append(QWeb.render("FieldMany2One.context_menu", {widget: self}));
|
||||
var bindings = {};
|
||||
|
||||
@@ -862,6 +862,8 @@ db.web.View = db.web.Widget.extend(/** @lends db.web.View# */{
|
||||
console.log('Todo');
|
||||
},
|
||||
on_sidebar_import: function() {
|
||||
var import_view = new db.web.DataImport(this, this.dataset);
|
||||
import_view.start();
|
||||
},
|
||||
on_sidebar_export: function() {
|
||||
var export_view = new db.web.DataExport(this, this.dataset);
|
||||
|
||||
@@ -1383,6 +1383,79 @@
|
||||
</table>
|
||||
</form>
|
||||
</t>
|
||||
|
||||
<t t-name="ImportView">
|
||||
<a id="importview" href="javascript: void(0)" style="text-decoration: none;color: #3D3D3D;">Import</a>
|
||||
</t>
|
||||
<t t-name="ImportDataView">
|
||||
<form name="import_data" id="import_data" action="" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="session_id" t-att-value="session.session_id"/>
|
||||
<h2 class="separator horizontal">1. Import a .CSV file</h2>
|
||||
<p>Select a .CSV file to import. If you need a sample of file to import,
|
||||
you should use the export tool with the "Import Compatible" option.
|
||||
</p>
|
||||
<p>
|
||||
<label for="csvfile">CSV File:</label>
|
||||
<input type="file" id="csvfile" size="50" name="csvfile"/>
|
||||
</p>
|
||||
<h2 class="separator horizontal">2. Check your file format</h2>
|
||||
<div id="result"></div>
|
||||
<fieldset>
|
||||
<legend style="cursor:pointer;">Import Options</legend>
|
||||
<table style="display:none">
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<label for="file_has_headers">Does your file have titles?</label>
|
||||
<input type="checkbox" checked="checked"
|
||||
id="file_has_headers"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="csv_separator">Separator:</label></td>
|
||||
<td><input type="text" name="csvsep" id="csv_separator" value=","/></td>
|
||||
<td><label for="csv_delimiter">Delimiter:</label></td>
|
||||
<td><input type="text" name="csvdel" id="csv_delimiter" value='"'/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label for="csv_encoding">Encoding:</label></td>
|
||||
<td>
|
||||
<select name="csvcode" id="csv_encoding">
|
||||
<option value="utf-8">UTF-8</option>
|
||||
<option value="latin1">Latin 1</option>
|
||||
</select>
|
||||
</td>
|
||||
<td><label for="csv_skip" title="For use if CSV files have titles on multiple lines, skips more than a single line during import">
|
||||
Lines to skip<sup>?</sup>:</label></td>
|
||||
<td><input type="number" id="csv_skip" value="0" min="0"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</form>
|
||||
</t>
|
||||
<table t-name="ImportView.result"
|
||||
class="oe_import_grid" width="100%" style="margin: 5px 0;">
|
||||
<tr t-if="headers" class="oe_import_grid-header">
|
||||
<td t-foreach="headers" t-as="header" class="oe_import_grid-cell">
|
||||
<t t-esc="header"/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td t-foreach="records[0]" t-as="column">
|
||||
<input class="sel_fields"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr t-foreach="records" t-as="record" class="oe_import_grid-row">
|
||||
<td t-foreach="record" t-as="cell" class="oe_import_grid-cell">
|
||||
<t t-esc="cell"/></td>
|
||||
</tr>
|
||||
</table>
|
||||
<t t-name="ImportView.error">
|
||||
<p style="white-space:pre-line;">The import failed due to:<t t-esc="error.message"/></p>
|
||||
<t t-if="error.preview">
|
||||
<p>Here is a preview of the file we could not import:</p>
|
||||
<pre><t t-esc="error.preview"/></pre>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
<t t-name="About-Page">
|
||||
<div>
|
||||
<h1>OpenERP Web</h1>
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ import web.common.dispatch
|
||||
|
||||
if __name__ == "__main__":
|
||||
(options, args) = optparser.parse_args(sys.argv[1:])
|
||||
options.backend = 'rpc'
|
||||
options.backend = 'xmlrpc'
|
||||
|
||||
os.environ["TZ"] = "UTC"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user