diff --git a/addons/base/__init__.py b/addons/base/__init__.py index ee5959455ad..343b53d050d 100644 --- a/addons/base/__init__.py +++ b/addons/base/__init__.py @@ -1 +1,6 @@ +import common import controllers + +# TODO +# if we detect that we are imported from the openerp server register common.Root() as a wsgi entry point + diff --git a/addons/base/common/__init__.py b/addons/base/common/__init__.py new file mode 100644 index 00000000000..60699c86f43 --- /dev/null +++ b/addons/base/common/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/python +# TODO if from openerpserver use backendlocal +# from backendlocal import * +from backendrpc import * +from dispatch import * diff --git a/openerpweb/ast.py b/addons/base/common/ast.py similarity index 100% rename from openerpweb/ast.py rename to addons/base/common/ast.py diff --git a/addons/base/common/backendlocal.py b/addons/base/common/backendlocal.py new file mode 100644 index 00000000000..dac33991c4d --- /dev/null +++ b/addons/base/common/backendlocal.py @@ -0,0 +1,23 @@ +#---------------------------------------------------------- +# OpenERPSession local openerp backend access +#---------------------------------------------------------- +class OpenERPUnboundException(Exception): + pass + +class OpenERPConnector(object): + pass + +class OpenERPAuth(object): + pass + +class OpenERPModel(object): + def __init__(self, session, model): + self._session = session + self._model = model + + def __getattr__(self, name): + return lambda *l:self._session.execute(self._model, name, *l) + +class OpenERPSession(object): + def __init__(self, model_factory=OpenERPModel): + pass diff --git a/addons/base/common/backendrpc.py b/addons/base/common/backendrpc.py new file mode 100644 index 00000000000..d2e2f6615e1 --- /dev/null +++ b/addons/base/common/backendrpc.py @@ -0,0 +1,212 @@ +#!/usr/bin/python +import datetime +import urllib +import dateutil.relativedelta +import functools +import logging +import optparse +import os +import sys +import tempfile +import time +import traceback +import uuid +import xmlrpclib + +import cherrypy +import cherrypy.lib.static +import simplejson + +import nonliterals +#---------------------------------------------------------- +# OpenERPSession RPC openerp backend access +#---------------------------------------------------------- +class OpenERPUnboundException(Exception): + pass + +class OpenERPConnector(object): + pass + +class OpenERPAuth(object): + pass + +class OpenERPModel(object): + def __init__(self, session, model): + self._session = session + self._model = model + + def __getattr__(self, name): + return lambda *l:self._session.execute(self._model, name, *l) + +class OpenERPSession(object): + """ + An OpenERP RPC session, a given user can own multiple such sessions + in a web session. + + .. attribute:: context + + The session context, a ``dict``. Can be reloaded by calling + :meth:`openerpweb.openerpweb.OpenERPSession.get_context` + + .. attribute:: domains_store + + A ``dict`` matching domain keys to evaluable (but non-literal) domains. + + 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, model_factory=OpenERPModel): + self._server = server + self._port = port + self._db = False + self._uid = False + self._login = False + self._password = False + self.model_factory = model_factory + self._locale = 'en_US' + self.context = {} + self.contexts_store = {} + self.domains_store = {} + self._lang = {} + self.remote_timezone = 'utc' + self.client_timezone = False + + def proxy(self, service): + s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) + return s + + def bind(self, db, uid, password): + self._db = db + self._uid = uid + self._password = password + + def login(self, db, login, password): + uid = self.proxy('common').login(db, login, password) + self.bind(db, uid, password) + self._login = login + + if uid: self.get_context() + return uid + + def assert_valid(self): + """ + Ensures this session is valid (logged into the openerp server) + """ + if not (self._db and self._uid and self._password): + raise OpenERPUnboundException() + + def execute(self, model, func, *l, **d): + self.assert_valid() + r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d) + return r + + def exec_workflow(self, model, id, signal): + self.assert_valid() + r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id) + return r + + def model(self, model): + """ Get an RPC proxy for the object ``model``, bound to this session. + + :param model: an OpenERP model name + :type model: str + :rtype: :class:`openerpweb.openerpweb.OpenERPModel` + """ + return self.model_factory(self, model) + + def get_context(self): + """ Re-initializes the current user's session context (based on + his preferences) by calling res.users.get_context() with the old + context + + :returns: the new context + """ + assert self._uid, "The user needs to be logged-in to initialize his context" + self.context = self.model('res.users').context_get(self.context) + self.context = self.context or {} + + self.client_timezone = self.context.get("tz", False) + # invalid code, anyway we decided the server will be in UTC + #if self.client_timezone: + # self.remote_timezone = self.execute('common', 'timezone_get') + + self._locale = self.context.get('lang','en_US') + lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)]) + if lang_ids: + self._lang = self.execute('res.lang', 'read',lang_ids[0], []) + return self.context + + @property + def base_eval_context(self): + """ Default evaluation context for the session. + + Used to evaluate contexts and domains. + """ + base = dict( + uid=self._uid, + current_date=datetime.date.today().strftime('%Y-%m-%d'), + time=time, + datetime=datetime, + relativedelta=dateutil.relativedelta.relativedelta + ) + base.update(self.context) + return base + + def evaluation_context(self, context=None): + """ Returns the session's evaluation context, augmented with the + provided context if any. + + :param dict context: to add merge in the session's base eval context + :returns: the augmented context + :rtype: dict + """ + d = dict(self.base_eval_context) + if context: + d.update(context) + d['context'] = d + return d + + def eval_context(self, context_to_eval, context=None): + """ Evaluates the provided context_to_eval in the context (haha) of + the context. Also merges the evaluated context with the session's context. + + :param context_to_eval: a context to evaluate. Must be a dict or a + non-literal context. If it's a dict, will be + returned as-is + :type context_to_eval: openerpweb.nonliterals.Context + :returns: the evaluated context + :rtype: dict + + :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor + a Context + """ + ctx = dict( + self.base_eval_context, + **(context or {})) + + # adding the context of the session to send to the openerp server + ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {}) + ccontext.session = self + return ccontext.evaluate(ctx) + + def eval_domain(self, domain, context=None): + """ Evaluates the provided domain using the provided context + (merged with the session's evaluation context) + + :param domain: an OpenERP domain as a list or as a + :class:`openerpweb.nonliterals.Domain` instance + + In the second case, it will be evaluated and returned. + :type domain: openerpweb.nonliterals.Domain + :param dict context: the context to use in the evaluation, if any. + :returns: the evaluated domain + :rtype: list + + :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain + """ + if isinstance(domain, list): + return domain + + cdomain = nonliterals.CompoundDomain(domain) + cdomain.session = self + return cdomain.evaluate(context or {}) diff --git a/openerpweb/dates.py b/addons/base/common/dates.py similarity index 100% rename from openerpweb/dates.py rename to addons/base/common/dates.py diff --git a/openerpweb/openerpweb.py b/addons/base/common/dispatch.py similarity index 61% rename from openerpweb/openerpweb.py rename to addons/base/common/dispatch.py index b2364692203..7d50785bf56 100644 --- a/openerpweb/openerpweb.py +++ b/addons/base/common/dispatch.py @@ -3,6 +3,7 @@ import datetime import urllib import dateutil.relativedelta import functools +import logging import optparse import os import sys @@ -17,14 +18,18 @@ import cherrypy.lib.static import simplejson import nonliterals -import logging +# TODO if from openerpserver use backendlocal +# from backendlocal import * +from backendrpc import * #----------------------------------------------------------- # Globals #----------------------------------------------------------- -path_root = os.path.dirname(os.path.dirname(os.path.normpath(__file__))) -path_addons = os.path.join(path_root, 'addons') +import __main__ + +path_root = __main__.path_root +path_addons = __main__.path_addons cherrypy_root = None #----------------------------------------------------------- @@ -38,199 +43,6 @@ controllers_class = {} controllers_object = {} controllers_path = {} -#---------------------------------------------------------- -# OpenERP Client Library -#---------------------------------------------------------- -class OpenERPUnboundException(Exception): - pass - -class OpenERPConnector(object): - pass - -class OpenERPAuth(object): - pass - -class OpenERPModel(object): - def __init__(self, session, model): - self._session = session - self._model = model - - def __getattr__(self, name): - return lambda *l:self._session.execute(self._model, name, *l) - -class OpenERPSession(object): - """ - An OpenERP RPC session, a given user can own multiple such sessions - in a web session. - - .. attribute:: context - - The session context, a ``dict``. Can be reloaded by calling - :meth:`openerpweb.openerpweb.OpenERPSession.get_context` - - .. attribute:: domains_store - - A ``dict`` matching domain keys to evaluable (but non-literal) domains. - - 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, model_factory=OpenERPModel): - self._server = server - self._port = port - self._db = False - self._uid = False - self._login = False - self._password = False - self.model_factory = model_factory - self._locale = 'en_US' - self.context = {} - self.contexts_store = {} - self.domains_store = {} - self._lang = {} - self.remote_timezone = 'utc' - self.client_timezone = False - - def proxy(self, service): - s = xmlrpclib.ServerProxy('http://%s:%s/xmlrpc/%s' % (self._server, self._port, service)) - return s - - def bind(self, db, uid, password): - self._db = db - self._uid = uid - self._password = password - - def login(self, db, login, password): - uid = self.proxy('common').login(db, login, password) - self.bind(db, uid, password) - self._login = login - - if uid: self.get_context() - return uid - - def assert_valid(self): - """ - Ensures this session is valid (logged into the openerp server) - """ - if not (self._db and self._uid and self._password): - raise OpenERPUnboundException() - - def execute(self, model, func, *l, **d): - self.assert_valid() - r = self.proxy('object').execute(self._db, self._uid, self._password, model, func, *l, **d) - return r - - def exec_workflow(self, model, id, signal): - self.assert_valid() - r = self.proxy('object').exec_workflow(self._db, self._uid, self._password, model, signal, id) - return r - - def model(self, model): - """ Get an RPC proxy for the object ``model``, bound to this session. - - :param model: an OpenERP model name - :type model: str - :rtype: :class:`openerpweb.openerpweb.OpenERPModel` - """ - return self.model_factory(self, model) - - def get_context(self): - """ Re-initializes the current user's session context (based on - his preferences) by calling res.users.get_context() with the old - context - - :returns: the new context - """ - assert self._uid, "The user needs to be logged-in to initialize his context" - self.context = self.model('res.users').context_get(self.context) - self.context = self.context or {} - - self.client_timezone = self.context.get("tz", False) - # invalid code, anyway we decided the server will be in UTC - #if self.client_timezone: - # self.remote_timezone = self.execute('common', 'timezone_get') - - self._locale = self.context.get('lang','en_US') - lang_ids = self.execute('res.lang','search', [('code', '=', self._locale)]) - if lang_ids: - self._lang = self.execute('res.lang', 'read',lang_ids[0], []) - return self.context - - @property - def base_eval_context(self): - """ Default evaluation context for the session. - - Used to evaluate contexts and domains. - """ - base = dict( - uid=self._uid, - current_date=datetime.date.today().strftime('%Y-%m-%d'), - time=time, - datetime=datetime, - relativedelta=dateutil.relativedelta.relativedelta - ) - base.update(self.context) - return base - - def evaluation_context(self, context=None): - """ Returns the session's evaluation context, augmented with the - provided context if any. - - :param dict context: to add merge in the session's base eval context - :returns: the augmented context - :rtype: dict - """ - d = dict(self.base_eval_context) - if context: - d.update(context) - d['context'] = d - return d - - def eval_context(self, context_to_eval, context=None): - """ Evaluates the provided context_to_eval in the context (haha) of - the context. Also merges the evaluated context with the session's context. - - :param context_to_eval: a context to evaluate. Must be a dict or a - non-literal context. If it's a dict, will be - returned as-is - :type context_to_eval: openerpweb.nonliterals.Context - :returns: the evaluated context - :rtype: dict - - :raises: ``TypeError`` if ``context_to_eval`` is neither a dict nor - a Context - """ - ctx = dict( - self.base_eval_context, - **(context or {})) - - # adding the context of the session to send to the openerp server - ccontext = nonliterals.CompoundContext(self.context, context_to_eval or {}) - ccontext.session = self - return ccontext.evaluate(ctx) - - def eval_domain(self, domain, context=None): - """ Evaluates the provided domain using the provided context - (merged with the session's evaluation context) - - :param domain: an OpenERP domain as a list or as a - :class:`openerpweb.nonliterals.Domain` instance - - In the second case, it will be evaluated and returned. - :type domain: openerpweb.nonliterals.Domain - :param dict context: the context to use in the evaluation, if any. - :returns: the evaluated domain - :rtype: list - - :raises: ``TypeError`` if ``domain`` is neither a list nor a Domain - """ - if isinstance(domain, list): - return domain - - cdomain = nonliterals.CompoundDomain(domain) - cdomain.session = self - return cdomain.evaluate(context or {}) - #---------------------------------------------------------- # OpenERP Web RequestHandler #---------------------------------------------------------- @@ -425,12 +237,16 @@ class Root(object): controllers_path[o._cp_path] = o def default(self, *l, **kw): - #print "default",l,kw + print "default",l,kw # handle static files if len(l) > 2 and l[1] == 'static': # sanitize path p = os.path.normpath(os.path.join(*l)) - return cherrypy.lib.static.serve_file(os.path.join(path_addons, p)) + p2 = os.path.join(path_addons, p) + print "p",p + print "p2",p2 + + return cherrypy.lib.static.serve_file(p2) elif len(l) > 1: for i in range(len(l), 1, -1): ps = "/" + "/".join(l[0:i]) diff --git a/openerpweb/nonliterals.py b/addons/base/common/nonliterals.py similarity index 100% rename from openerpweb/nonliterals.py rename to addons/base/common/nonliterals.py diff --git a/openerpweb/tests/__init__.py b/addons/base/common/tests/__init__.py similarity index 100% rename from openerpweb/tests/__init__.py rename to addons/base/common/tests/__init__.py diff --git a/openerpweb/tests/test_model.py b/addons/base/common/tests/test_model.py similarity index 100% rename from openerpweb/tests/test_model.py rename to addons/base/common/tests/test_model.py diff --git a/openerpweb/tests/test_nonliterals.py b/addons/base/common/tests/test_nonliterals.py similarity index 100% rename from openerpweb/tests/test_nonliterals.py rename to addons/base/common/tests/test_nonliterals.py diff --git a/openerpweb/tests/test_session.py b/addons/base/common/tests/test_session.py similarity index 100% rename from openerpweb/tests/test_session.py rename to addons/base/common/tests/test_session.py diff --git a/addons/base/controllers/main.py b/addons/base/controllers/main.py index c56a66342f0..4b07cac867b 100644 --- a/addons/base/controllers/main.py +++ b/addons/base/controllers/main.py @@ -9,17 +9,22 @@ import re import simplejson import textwrap import xmlrpclib +import time from xml.etree import ElementTree from cStringIO import StringIO import cherrypy -import openerpweb -import openerpweb.ast -import openerpweb.nonliterals +import base.common as openerpweb +import base.common.ast +import base.common.nonliterals +openerpweb.ast = base.common.ast +openerpweb.nonliterals = base.common.nonliterals from babel.messages.pofile import read_po +_REPORT_POLLER_DELAY = 0.05 + # Should move to openerpweb.Xml2Json class Xml2Json: # xml2json-direct @@ -140,8 +145,7 @@ class WebClient(openerpweb.Controller): return content @openerpweb.httprequest - def home(self, req, s_action=None): - + def home(self, req, s_action=None, **kw): # script tags jslist = ['/base/webclient/js'] if req.debug: @@ -433,9 +437,12 @@ def load_actions_from_ir_values(req, key, key2, models, meta): for id, name, action in actions] def clean_action(req, action): + action.setdefault('flags', {}) + if action['type'] != 'ir.actions.act_window': + return action + context = req.session.eval_context(req.context) eval_ctx = req.session.evaluation_context(context) - action.setdefault('flags', {}) # values come from the server, we can just eval them if isinstance(action.get('context'), basestring): @@ -1275,6 +1282,29 @@ class Export(View): else: return export_csv(field, result) +class Export(View): + _cp_path = "/base/report" + + @openerpweb.jsonrequest + def get_report(self, req, action): + report_srv = req.session.proxy("report") + context = req.session.eval_context(openerpweb.nonliterals.CompoundContext(req.context, \ + action["context"])) + + args = [req.session._db, req.session._uid, req.session._password, action["report_name"], context["active_ids"], {"id": context["active_id"], "model": context["active_model"], "report_type": action["report_type"]}, context] + report_id = report_srv.report(*args) + report = None + while True: + args2 = [req.session._db, req.session._uid, req.session._password, report_id] + report = report_srv.report_get(*args2) + if report["state"]: + break + + time.sleep(_REPORT_POLLER_DELAY) + #TODO: ok now we've got the report, and so what? + return False + + class Import(View): _cp_path = "/base/import" @@ -1347,7 +1377,6 @@ class Import(View): else: fields.append((word, word)) # error = {'message':("You cannot import the field '%s', because we cannot auto-detect it" % (word,))} - break except: error = {'message':('Error processing the first line of the file. Field "%s" is unknown') % (word,)} diff --git a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js index cb456eed14c..2356f3b606b 100644 --- a/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js +++ b/addons/base/static/lib/jquery.contextmenu/jquery.contextmenu.r2.packed.js @@ -32,7 +32,7 @@ if(!menu) { menu=$('
').hide().css({ position:'absolute', - zIndex:'500' + zIndex:'2000' }).appendTo('body').bind('click', function(e) { e.stopPropagation() }) diff --git a/addons/base/static/src/css/base.css b/addons/base/static/src/css/base.css index 46364c4fa1d..4133a9c8623 100644 --- a/addons/base/static/src/css/base.css +++ b/addons/base/static/src/css/base.css @@ -597,13 +597,13 @@ label.error { background-color: #ccc; } .openerp .searchview_group.folded .searchview_group_string { - background: url("../img/ui/group-folded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-folded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.folded .searchview_group_content { display: none; } .openerp .searchview_group.expanded .searchview_group_string { - background: url("../img/ui/group-expanded.png") no-repeat scroll 0 50%; + background: url("/base/static/src/img/ui/group-expanded.png") no-repeat scroll 0 50%; } .openerp .searchview_group.expanded .searchview_group_content { display: block; @@ -635,13 +635,13 @@ label.error { } .openerp .oe_search-view-custom-filter-btn span { - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } .openerp .searchview_extended_add_proposition span { font-size: 0.9em; - background: url(../img/icons/gtk-add.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-add.png) repeat-y; padding-left: 18px; } @@ -657,7 +657,7 @@ label.error { .openerp .searchview_extended_delete_group span, .openerp .searchview_extended_delete_prop span { font-size: 0.9em; - background: url(../img/icons/gtk-close.png) repeat-y; + background: url(/base/static/src/img/icons/gtk-close.png) repeat-y; padding-left: 18px; } /* List */ @@ -895,6 +895,9 @@ label.error { position: relative; top: 4px; } +.openerp .oe_trad_field.touched { + border: 1px solid green !important; +} /* http://www.quirksmode.org/dom/inputfile.html * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image @@ -1088,7 +1091,7 @@ label.error { cursor: pointer; border: 1px solid #D2CFCF; display: block; - background: url(../img/toggle-a-bg.png); + background: url(/base/static/src/img/toggle-a-bg.png); width: 21px; height: 21px; z-index: 10; @@ -1103,7 +1106,7 @@ label.error { } .openerp li.oe_sidebar_print { padding-left: 20px; - background: 1px 3px url(../img/icons/gtk-print.png) no-repeat; + background: 1px 3px url(/base/static/src/img/icons/gtk-print.png) no-repeat; } .openerp.kitten-mode-activated .main_table { diff --git a/addons/base/static/src/js/boot.js b/addons/base/static/src/js/boot.js index 5ae30611195..43f52dfac85 100644 --- a/addons/base/static/src/js/boot.js +++ b/addons/base/static/src/js/boot.js @@ -50,7 +50,9 @@ openerp.base = function(instance) { openerp.base.core(instance); - openerp.base.dates(instance); + if (openerp.base.dates) { + openerp.base.dates(instance); + } openerp.base.formats(instance); openerp.base.chrome(instance); openerp.base.data(instance); diff --git a/addons/base/static/src/js/chrome.js b/addons/base/static/src/js/chrome.js index b1b9df73557..a0dd0951748 100644 --- a/addons/base/static/src/js/chrome.js +++ b/addons/base/static/src/js/chrome.js @@ -479,8 +479,6 @@ openerp.base.Login = openerp.base.Widget.extend({ this.selected_login = localStorage.getItem('last_login_login_success'); } if (jQuery.deparam(jQuery.param.querystring()).debug != undefined) { - this.selected_db = this.selected_db || "trunk"; - this.selected_login = this.selected_login || "admin"; this.selected_password = this.selected_password || "a"; } }, @@ -564,22 +562,25 @@ openerp.base.Login = openerp.base.Widget.extend({ }); openerp.base.Header = openerp.base.Widget.extend({ - init: function(parent, element_id) { - this._super(parent, element_id); - if (jQuery.deparam(jQuery.param.querystring()).debug !== undefined) { - this.qs = '?debug' - } else { - this.qs = '' - } + template: "Header", + identifier_prefix: 'oe-app-header-', + init: function(parent) { + this._super(parent); + this.qs = "?" + jQuery.param.querystring(); + this.$content = $(); }, start: function() { - return this.do_update(); + this._super(); }, do_update: function () { - this.$element.html(QWeb.render("Header", this)); + this.$content = $(QWeb.render("Header-content", {widget: this})); + this.$content.appendTo(this.$element); this.$element.find(".logout").click(this.on_logout); return this.shortcut_load(); }, + do_reset: function() { + this.$content.remove(); + }, shortcut_load :function(){ var self = this, sc = self.session.shortcuts, @@ -590,22 +591,27 @@ openerp.base.Header = openerp.base.Widget.extend({ sc.binding = {}; $(sc.binding).bind({ 'add': function (e, attrs) { - var $shortcut = $('