From 04e9c7a19fbe13594478ca412451f0c1f10c74d0 Mon Sep 17 00:00:00 2001 From: niv-openerp Date: Fri, 18 Oct 2013 14:12:43 +0200 Subject: [PATCH 1/8] Removed small usage of openerp.http bzr revid: nicolas.vanhoren@openerp.com-20131018121243-o1ua7s85x75fwyuq --- addons/edi/edi_service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/addons/edi/edi_service.py b/addons/edi/edi_service.py index d5dc5c09f74..dd95196ef4f 100644 --- a/addons/edi/edi_service.py +++ b/addons/edi/edi_service.py @@ -53,7 +53,6 @@ def exp_import_edi_document(db_name, uid, passwd, edi_document, context=None): def exp_import_edi_url(db_name, uid, passwd, edi_url, context=None): return _edi_dispatch(db_name, 'import_edi', uid, None, edi_url) -@openerp.http.rpc('edi') def dispatch(method, params): if method in ['import_edi_document', 'import_edi_url']: (db, uid, passwd) = params[0:3] @@ -63,4 +62,6 @@ def dispatch(method, params): fn = globals()['exp_' + method] return fn(*params) +openerp.service.wsgi_server.register_rpc_endpoint('edi', dispatch) + # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: From 3d8b7984bece705354e774213064c9540f5280c5 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Tue, 22 Oct 2013 19:06:59 +0200 Subject: [PATCH 2/8] import cleanups part1 bzr revid: al@openerp.com-20131022170659-kg9err029ha22evh --- addons/auth_oauth/controllers/main.py | 4 ++-- addons/auth_openid/controllers/main.py | 4 ++-- addons/auth_signup/controllers/main.py | 4 ++-- addons/edi/controllers/main.py | 15 ++++++++------- addons/im/im.py | 16 ++++++++-------- addons/im_livechat/im_livechat.py | 9 +++++---- addons/mail/controllers/main.py | 2 +- addons/mass_mailing/controllers/main.py | 5 ++--- addons/point_of_sale/controllers/main.py | 4 ++-- 9 files changed, 32 insertions(+), 31 deletions(-) diff --git a/addons/auth_oauth/controllers/main.py b/addons/auth_oauth/controllers/main.py index 35a06871696..bedf32ae776 100644 --- a/addons/auth_oauth/controllers/main.py +++ b/addons/auth_oauth/controllers/main.py @@ -7,8 +7,8 @@ from werkzeug.exceptions import BadRequest import openerp from openerp import SUPERUSER_ID -import openerp.addons.web.http as http -from openerp.addons.web.http import request +from openerp import http +from openerp.http import request from openerp.addons.web.controllers.main import db_monodb, set_cookie_and_redirect, login_and_redirect from openerp.modules.registry import RegistryManager diff --git a/addons/auth_openid/controllers/main.py b/addons/auth_openid/controllers/main.py index a4ef1bfa07b..534d1b01a9c 100644 --- a/addons/auth_openid/controllers/main.py +++ b/addons/auth_openid/controllers/main.py @@ -38,8 +38,8 @@ import openerp from openerp import SUPERUSER_ID from openerp.modules.registry import RegistryManager from openerp.addons.web.controllers.main import login_and_redirect, set_cookie_and_redirect -import openerp.addons.web.http as http -from openerp.addons.web.http import request +import openerp.http as http +from openerp.http import request from .. import utils diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index e42dc64151f..a778cbcf061 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -21,10 +21,10 @@ import logging import openerp +from openerp import http +from openerp.http import request from openerp.modules.registry import RegistryManager from ..res_users import SignupError -import openerp.addons.web.http as http -from openerp.addons.web.http import request _logger = logging.getLogger(__name__) diff --git a/addons/edi/controllers/main.py b/addons/edi/controllers/main.py index 7e27428b63b..3f77cced386 100644 --- a/addons/edi/controllers/main.py +++ b/addons/edi/controllers/main.py @@ -1,15 +1,15 @@ import simplejson import urllib -import openerp.addons.web.http as openerpweb +import openerp import openerp.addons.web.controllers.main as webmain class EDI(openerpweb.Controller): - # http://hostname:8069/edi/import_url?url=URIEncodedURL - _cp_path = "/edi" - @openerpweb.httprequest - def import_url(self, req, url): + @openerp.http.route('/edi/import_url', type='http', auth='none') + def import_url(self, url): + # http://hostname:8069/edi/import_url?url=URIEncodedURL + req = openerp.http.request modules = webmain.module_boot(req) + ['edi'] modules_str = ','.join(modules) modules_json = simplejson.dumps(modules) @@ -26,8 +26,9 @@ class EDI(openerpweb.Controller): 'init': 's.edi.edi_import("%s");' % safe_url, } - @openerpweb.jsonrequest - def import_edi_url(self, req, url): + @openerp.http.route('/edi/import_edi_url', type='http', auth='none') + def import_edi_url(self, url): + req = openerp.http.request result = req.session.proxy('edi').import_edi_url(req.session._db, req.session._uid, req.session._password, url) if len(result) == 1: return {"action": webmain.clean_action(req, result[0][2])} diff --git a/addons/im/im.py b/addons/im/im.py index 3c4f0bfcd14..fd87dbacbff 100644 --- a/addons/im/im.py +++ b/addons/im/im.py @@ -18,19 +18,19 @@ # along with this program. If not, see . # ############################################################################## +import datetime +import json +import logging +import select +import time import openerp import openerp.tools.config import openerp.modules.registry -import openerp.addons.web.http as http -from openerp.addons.web.http import request -from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT -import datetime +from openerp import http +from openerp.http import request from openerp.osv import osv, fields -import time -import logging -import json -import select +from openerp.tools.misc import DEFAULT_SERVER_DATETIME_FORMAT _logger = logging.getLogger(__name__) diff --git a/addons/im_livechat/im_livechat.py b/addons/im_livechat/im_livechat.py index 81d3d61c6b4..cf8060596d7 100644 --- a/addons/im_livechat/im_livechat.py +++ b/addons/im_livechat/im_livechat.py @@ -19,15 +19,16 @@ # ############################################################################## -import openerp -import openerp.addons.im.im as im import json import random import jinja2 + +import openerp +import openerp.addons.im.im as im from openerp.osv import osv, fields from openerp import tools -import openerp.addons.web.http as http -from openerp.addons.web.http import request +from openerp.http as http +from openerp.http import request env = jinja2.Environment( loader=jinja2.PackageLoader('openerp.addons.im_livechat', "."), diff --git a/addons/mail/controllers/main.py b/addons/mail/controllers/main.py index c5ef8bac92d..c2e15c4a12f 100644 --- a/addons/mail/controllers/main.py +++ b/addons/mail/controllers/main.py @@ -3,7 +3,7 @@ import psycopg2 import openerp from openerp import SUPERUSER_ID -import openerp.addons.web.http as http +from openerp import http from openerp.addons.web.controllers.main import content_disposition diff --git a/addons/mass_mailing/controllers/main.py b/addons/mass_mailing/controllers/main.py index 3ca238256b1..ac1531d7192 100644 --- a/addons/mass_mailing/controllers/main.py +++ b/addons/mass_mailing/controllers/main.py @@ -1,7 +1,6 @@ -import openerp.addons.web.http as http -from openerp.addons.web.http import request - +from openerp import http +from openerp.http import request class MassMailController(http.Controller): @http.route('/mail/track//blank.gif', type='http', auth='admin') diff --git a/addons/point_of_sale/controllers/main.py b/addons/point_of_sale/controllers/main.py index 419f5bc2efa..2461fe90b79 100644 --- a/addons/point_of_sale/controllers/main.py +++ b/addons/point_of_sale/controllers/main.py @@ -6,8 +6,8 @@ import openerp import time import random -from openerp.addons.web import http -from openerp.addons.web.http import request +from openerp import http +from openerp.http import request from openerp.addons.web.controllers.main import manifest_list, module_boot, html_template _logger = logging.getLogger(__name__) From 2e3af6f3d0f799e4779f3d5e62bc7bfa118242f3 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Wed, 23 Oct 2013 00:52:47 +0200 Subject: [PATCH 3/8] import cleanups part2 bzr revid: al@openerp.com-20131022225247-3zv7hjz1iidnb6zl --- addons/edi/controllers/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/edi/controllers/main.py b/addons/edi/controllers/main.py index 3f77cced386..149c0f62ff6 100644 --- a/addons/edi/controllers/main.py +++ b/addons/edi/controllers/main.py @@ -4,7 +4,7 @@ import urllib import openerp import openerp.addons.web.controllers.main as webmain -class EDI(openerpweb.Controller): +class EDI(openerp.http.Controller): @openerp.http.route('/edi/import_url', type='http', auth='none') def import_url(self, url): From 737586aa4e6f21540c8f18b8e02f2f9cd389021a Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 27 Oct 2013 17:40:23 +0100 Subject: [PATCH 4/8] typo bzr revid: al@openerp.com-20131027164023-ky1nthy56fmqr4f2 --- addons/im_livechat/im_livechat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/im_livechat/im_livechat.py b/addons/im_livechat/im_livechat.py index cf8060596d7..b21a31d8362 100644 --- a/addons/im_livechat/im_livechat.py +++ b/addons/im_livechat/im_livechat.py @@ -27,7 +27,7 @@ import openerp import openerp.addons.im.im as im from openerp.osv import osv, fields from openerp import tools -from openerp.http as http +from openerp import http from openerp.http import request env = jinja2.Environment( From a56909360679351c5148038259c00a89161c0bc0 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 27 Oct 2013 18:15:53 +0100 Subject: [PATCH 5/8] [IMP] move http layer from openerp-web to openobject-server, convert controllers to the new route api versioned history http.py can be found in lp:~openerp/openerp-web/trunk file addons/web/http.py bzr revid: al@openerp.com-20131027171553-qe1ulbff0bphm15o --- openerp/http.py | 1126 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1098 insertions(+), 28 deletions(-) diff --git a/openerp/http.py b/openerp/http.py index e2f3024110d..f84d17cebf6 100644 --- a/openerp/http.py +++ b/openerp/http.py @@ -1,37 +1,1107 @@ # -*- coding: utf-8 -*- +#---------------------------------------------------------- +# OpenERP HTTP layer +#---------------------------------------------------------- +import ast +import cgi +import contextlib +import errno +import functools +import getpass +import inspect +import logging +import mimetypes +import os +import random +import re +import sys +import tempfile +import threading +import time +import traceback +import urlparse +import warnings +import babel.core +import simplejson +import werkzeug.contrib.sessions +import werkzeug.datastructures +import werkzeug.exceptions +import werkzeug.wrappers +import werkzeug.wsgi +import werkzeug.routing as routing + +import openerp +from openerp.service import security, model as service_model +from openerp.tools import config + +_logger = logging.getLogger(__name__) + +#---------------------------------------------------------- +# RequestHandler +#---------------------------------------------------------- +class WebRequest(object): + """ Parent class for all OpenERP Web request types, mostly deals with + initialization and setup of the request object (the dispatching itself has + to be handled by the subclasses) + + :param request: a wrapped werkzeug Request object + :type request: :class:`werkzeug.wrappers.BaseRequest` + + .. attribute:: httprequest + + the original :class:`werkzeug.wrappers.Request` object provided to the + request + + .. attribute:: httpsession + + .. deprecated:: 8.0 + + Use ``self.session`` instead. + + .. attribute:: params + + :class:`~collections.Mapping` of request parameters, not generally + useful as they're provided directly to the handler method as keyword + arguments + + .. attribute:: session_id + + opaque identifier for the :class:`session.OpenERPSession` instance of + the current request + + .. attribute:: session + + a :class:`OpenERPSession` holding the HTTP session data for the + current http session + + .. attribute:: context + + :class:`~collections.Mapping` of context values for the current request + + .. attribute:: db + + ``str``, the name of the database linked to the current request. Can be ``None`` + if the current request uses the ``none`` authentication. + + .. attribute:: uid + + ``int``, the id of the user related to the current request. Can be ``None`` + if the current request uses the ``none`` authenticatoin. + """ + def __init__(self, httprequest): + self.httprequest = httprequest + self.httpresponse = None + self.httpsession = httprequest.session + self.session = httprequest.session + self.session_id = httprequest.session.sid + self.disable_db = False + self.uid = None + self.func = None + self.auth_method = None + self._cr_cm = None + self._cr = None + self.func_request_type = None + # set db/uid trackers - they're cleaned up at the WSGI + # dispatching phase in openerp.service.wsgi_server.application + if self.db: + threading.current_thread().dbname = self.db + if self.session.uid: + threading.current_thread().uid = self.session.uid + self.context = dict(self.session.context) + self.lang = self.context["lang"] + + def _authenticate(self): + if self.session.uid: + try: + self.session.check_security() + except SessionExpiredException, e: + self.session.logout() + raise SessionExpiredException("Session expired for request %s" % self.httprequest) + auth_methods[self.auth_method]() + @property + def registry(self): + """ + The registry to the database linked to this request. Can be ``None`` if the current request uses the + ``none'' authentication. + """ + return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None + + @property + def db(self): + """ + The registry to the database linked to this request. Can be ``None`` if the current request uses the + ``none'' authentication. + """ + return self.session.db if not self.disable_db else None + + @property + def cr(self): + """ + The cursor initialized for the current method call. If the current request uses the ``none`` authentication + trying to access this property will raise an exception. + """ + # some magic to lazy create the cr + if not self._cr_cm: + self._cr_cm = self.registry.cursor() + self._cr = self._cr_cm.__enter__() + return self._cr + + def _call_function(self, *args, **kwargs): + self._authenticate() + try: + # ugly syntax only to get the __exit__ arguments to pass to self._cr + request = self + class with_obj(object): + def __enter__(self): + pass + def __exit__(self, *args): + if request._cr_cm: + request._cr_cm.__exit__(*args) + request._cr_cm = None + request._cr = None + + with with_obj(): + if self.func_request_type != self._request_type: + raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ + % (self.func, self.httprequest.path, self.func_request_type, self._request_type)) + return self.func(*args, **kwargs) + finally: + # just to be sure no one tries to re-use the request + self.disable_db = True + self.uid = None + + @property + def debug(self): + return 'debug' in self.httprequest.args + + @contextlib.contextmanager + def registry_cr(self): + warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) + yield (self.registry, self.cr) + +def auth_method_user(): + request.uid = request.session.uid + if not request.uid: + raise SessionExpiredException("Session expired") + +def auth_method_admin(): + if not request.db: + raise SessionExpiredException("No valid database for request %s" % request.httprequest) + request.uid = openerp.SUPERUSER_ID + +def auth_method_none(): + request.disable_db = True + request.uid = None + +auth_methods = { + "user": auth_method_user, + "admin": auth_method_admin, + "none": auth_method_none, +} + +def route(route, type="http", auth="user"): + """ + Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass + of ``Controller``. + + :param route: string or array. The route part that will determine which http requests will match the decorated + method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of + route expression ( http://werkzeug.pocoo.org/docs/routing/ ). + :param type: The type of request, can be ``'http'`` or ``'json'``. + :param auth: The type of authentication method, can on of the following: + + * ``user``: The user must be authenticated and the current request will perform using the rights of the + user. + * ``admin``: The user may not be authenticated and the current request will perform using the admin user. + * ``none``: The method is always active, even if there is no database. Mainly used by the framework and + authentication modules. There request code will not have any facilities to access the database nor have any + configuration indicating the current database nor the current user. + """ + assert type in ["http", "json"] + assert auth in auth_methods.keys() + def decorator(f): + if isinstance(route, list): + f.routes = route + else: + f.routes = [route] + f.exposed = type + if getattr(f, "auth", None) is None: + f.auth = auth + return f + return decorator + +def reject_nonliteral(dct): + if '__ref' in dct: + raise ValueError( + "Non literal contexts can not be sent to the server anymore (%r)" % (dct,)) + return dct + +class JsonRequest(WebRequest): + """ JSON-RPC2 over HTTP. + + Sucessful request:: + + --> {"jsonrpc": "2.0", + "method": "call", + "params": {"context": {}, + "arg1": "val1" }, + "id": null} + + <-- {"jsonrpc": "2.0", + "result": { "res1": "val1" }, + "id": null} + + Request producing a error:: + + --> {"jsonrpc": "2.0", + "method": "call", + "params": {"context": {}, + "arg1": "val1" }, + "id": null} + + <-- {"jsonrpc": "2.0", + "error": {"code": 1, + "message": "End user error message.", + "data": {"code": "codestring", + "debug": "traceback" } }, + "id": null} + + """ + _request_type = "json" + + def __init__(self, *args): + super(JsonRequest, self).__init__(*args) + + self.jsonp_handler = None + + args = self.httprequest.args + jsonp = args.get('jsonp') + self.jsonp = jsonp + request = None + request_id = args.get('id') + + if jsonp and self.httprequest.method == 'POST': + # jsonp 2 steps step1 POST: save call + def handler(): + self.session.jsonp_requests[request_id] = self.httprequest.form['r'] + self.session.modified = True + headers=[('Content-Type', 'text/plain; charset=utf-8')] + r = werkzeug.wrappers.Response(request_id, headers=headers) + return r + self.jsonp_handler = handler + return + elif jsonp and args.get('r'): + # jsonp method GET + request = args.get('r') + elif jsonp and request_id: + # jsonp 2 steps step2 GET: run and return result + request = self.session.jsonp_requests.pop(request_id, "") + else: + # regular jsonrpc2 + request = self.httprequest.stream.read() + + # Read POST content or POST Form Data named "request" + self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral) + self.params = dict(self.jsonrequest.get("params", {})) + self.context = self.params.pop('context', self.session.context) + + def dispatch(self): + """ Calls the method asked for by the JSON-RPC2 or JSONP request + """ + if self.jsonp_handler: + return self.jsonp_handler() + response = {"jsonrpc": "2.0" } + error = None + + try: + response['id'] = self.jsonrequest.get('id') + response["result"] = self._call_function(**self.params) + except AuthenticationError, e: + _logger.exception("Exception during JSON request handling.") + se = serialize_exception(e) + error = { + 'code': 100, + 'message': "OpenERP Session Invalid", + 'data': se + } + except Exception, e: + _logger.exception("Exception during JSON request handling.") + se = serialize_exception(e) + error = { + 'code': 200, + 'message': "OpenERP Server Error", + 'data': se + } + if error: + response["error"] = error + + if self.jsonp: + # If we use jsonp, that's mean we are called from another host + # Some browser (IE and Safari) do no allow third party cookies + # We need then to manage http sessions manually. + response['session_id'] = self.session_id + mime = 'application/javascript' + body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),) + else: + mime = 'application/json' + body = simplejson.dumps(response) + + r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) + return r + +def serialize_exception(e): + tmp = { + "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__, + "debug": traceback.format_exc(), + "message": u"%s" % e, + "arguments": to_jsonable(e.args), + } + if isinstance(e, openerp.osv.osv.except_osv): + tmp["exception_type"] = "except_osv" + elif isinstance(e, openerp.exceptions.Warning): + tmp["exception_type"] = "warning" + elif isinstance(e, openerp.exceptions.AccessError): + tmp["exception_type"] = "access_error" + elif isinstance(e, openerp.exceptions.AccessDenied): + tmp["exception_type"] = "access_denied" + return tmp + +def to_jsonable(o): + if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \ + or isinstance(o, bool) or o is None or isinstance(o, float): + return o + if isinstance(o, list) or isinstance(o, tuple): + return [to_jsonable(x) for x in o] + if isinstance(o, dict): + tmp = {} + for k, v in o.items(): + tmp[u"%s" % k] = to_jsonable(v) + return tmp + return u"%s" % o + +def jsonrequest(f): + """ + .. deprecated:: 8.0 + + Use the ``route()`` decorator instead. + """ + f.combine = True + base = f.__name__.lstrip('/') + if f.__name__ == "index": + base = "" + return route([base, base + "/"], type="json", auth="user")(f) + +class HttpRequest(WebRequest): + """ Regular GET/POST request + """ + _request_type = "http" + + def __init__(self, *args): + super(HttpRequest, self).__init__(*args) + params = dict(self.httprequest.args) + params.update(self.httprequest.form) + params.update(self.httprequest.files) + params.pop('session_id', None) + self.params = params + + def dispatch(self): + try: + r = self._call_function(**self.params) + except werkzeug.exceptions.HTTPException, e: + r = e + except Exception, e: + _logger.exception("An exception occured during an http request") + se = serialize_exception(e) + error = { + 'code': 200, + 'message': "OpenERP Server Error", + 'data': se + } + r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error))) + else: + if not r: + r = werkzeug.wrappers.Response(status=204) # no content + return r + + def make_response(self, data, headers=None, cookies=None): + """ Helper for non-HTML responses, or HTML responses with custom + response headers or cookies. + + While handlers can just return the HTML markup of a page they want to + send as a string if non-HTML data is returned they need to create a + complete response object, or the returned data will not be correctly + interpreted by the clients. + + :param basestring data: response body + :param headers: HTTP headers to set on the response + :type headers: ``[(name, value)]`` + :param collections.Mapping cookies: cookies to set on the client + """ + response = werkzeug.wrappers.Response(data, headers=headers) + if cookies: + for k, v in cookies.iteritems(): + response.set_cookie(k, v) + return response + + def not_found(self, description=None): + """ Helper for 404 response, return its result from the method + """ + return werkzeug.exceptions.NotFound(description) + +def httprequest(f): + """ + .. deprecated:: 8.0 + + Use the ``route()`` decorator instead. + """ + f.combine = True + base = f.__name__.lstrip('/') + if f.__name__ == "index": + base = "" + return route([base, base + "/"], type="http", auth="user")(f) + +#---------------------------------------------------------- +# Thread local global request object +#---------------------------------------------------------- +from werkzeug.local import LocalStack + +_request_stack = LocalStack() + +request = _request_stack() """ -``openerp.http`` offers decorators to register WSGI and RPC endpoints handlers. -See :ref:`routing`. + A global proxy that always redirect to the current request object. """ -from . import service +@contextlib.contextmanager +def set_request(req): + _request_stack.push(req) + try: + yield + finally: + _request_stack.pop() -def handler(): - """ - Decorator to register a WSGI handler. The handler must return None if it - does not handle the request. - """ - def decorator(f): - service.wsgi_server.register_wsgi_handler(f) - return decorator +#---------------------------------------------------------- +# Controller metaclass registration +#---------------------------------------------------------- +addons_module = {} +addons_manifest = {} +controllers_per_module = {} -def route(url): - """ - Same as then handler() decorator but register the handler under a specific - url. Not yet implemented. - """ - def decorator(f): - pass # TODO - return decorator +class ControllerType(type): + def __init__(cls, name, bases, attrs): + super(ControllerType, cls).__init__(name, bases, attrs) -def rpc(endpoint): - """ - Decorator to register a RPC endpoint handler. The handler will receive - already unmarshalled RCP arguments. - """ - def decorator(f): - service.wsgi_server.register_rpc_endpoint(endpoint, f) - return decorator + # flag old-style methods with req as first argument + for k, v in attrs.items(): + if inspect.isfunction(v): + spec = inspect.getargspec(v) + first_arg = spec.args[1] if len(spec.args) >= 2 else None + if first_arg in ["req", "request"]: + v._first_arg_is_req = True -# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: + # store the controller in the controllers list + name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls) + class_path = name_class[0].split(".") + if not class_path[:2] == ["openerp", "addons"]: + module = "" + else: + # we want to know all modules that have controllers + module = class_path[2] + # but we only store controllers directly inheriting from Controller + if not "Controller" in globals() or not Controller in bases: + return + controllers_per_module.setdefault(module, []).append(name_class) + +class Controller(object): + __metaclass__ = ControllerType + +#---------------------------------------------------------- +# HTTP Sessions +#---------------------------------------------------------- +class AuthenticationError(Exception): + pass + +class SessionExpiredException(Exception): + pass + +class Service(object): + """ + .. deprecated:: 8.0 + Use ``openerp.netsvc.dispatch_rpc()`` instead. + """ + def __init__(self, session, service_name): + self.session = session + self.service_name = service_name + + def __getattr__(self, method): + def proxy_method(*args): + result = openerp.netsvc.dispatch_rpc(self.service_name, method, args) + return result + return proxy_method + +class Model(object): + """ + .. deprecated:: 8.0 + Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. + """ + def __init__(self, session, model): + self.session = session + self.model = model + self.proxy = self.session.proxy('object') + + def __getattr__(self, method): + self.session.assert_valid() + def proxy(*args, **kw): + # Can't provide any retro-compatibility for this case, so we check it and raise an Exception + # to tell the programmer to adapt his code + if not request.db or not request.uid or self.session.db != request.db \ + or self.session.uid != request.uid: + raise Exception("Trying to use Model with badly configured database or user.") + + mod = request.registry.get(self.model) + if method.startswith('_'): + raise Exception("Access denied") + meth = getattr(mod, method) + cr = request.cr + result = meth(cr, request.uid, *args, **kw) + # reorder read + if method == "read": + if isinstance(result, list) and len(result) > 0 and "id" in result[0]: + index = {} + for r in result: + index[r['id']] = r + result = [index[x] for x in args[0] if x in index] + return result + return proxy + +class OpenERPSession(werkzeug.contrib.sessions.Session): + def __init__(self, *args, **kwargs): + self.inited = False + self.modified = False + super(OpenERPSession, self).__init__(*args, **kwargs) + self.inited = True + self._default_values() + self.modified = False + + def __getattr__(self, attr): + return self.get(attr, None) + def __setattr__(self, k, v): + if getattr(self, "inited", False): + try: + object.__getattribute__(self, k) + except: + return self.__setitem__(k, v) + object.__setattr__(self, k, v) + + def authenticate(self, db, login=None, password=None, uid=None): + """ + Authenticate the current user with the given db, login and password. If successful, store + the authentication parameters in the current session and request. + + :param uid: If not None, that user id will be used instead the login to authenticate the user. + """ + + if uid is None: + wsgienv = request.httprequest.environ + env = dict( + base_location=request.httprequest.url_root.rstrip('/'), + HTTP_HOST=wsgienv['HTTP_HOST'], + REMOTE_ADDR=wsgienv['REMOTE_ADDR'], + ) + uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env]) + else: + security.check(db, uid, password) + self.db = db + self.uid = uid + self.login = login + self.password = password + request.uid = uid + request.disable_db = False + + if uid: self.get_context() + return uid + + def check_security(self): + """ + Chech the current authentication parameters to know if those are still valid. This method + should be called at each request. If the authentication fails, a ``SessionExpiredException`` + is raised. + """ + if not self.db or not self.uid: + raise SessionExpiredException("Session expired") + security.check(self.db, self.uid, self.password) + + def logout(self): + for k in self.keys(): + del self[k] + self._default_values() + + def _default_values(self): + self.setdefault("db", None) + self.setdefault("uid", None) + self.setdefault("login", None) + self.setdefault("password", None) + self.setdefault("context", {'tz': "UTC", "uid": None}) + self.setdefault("jsonp_requests", {}) + + 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 = request.registry.get('res.users').context_get(request.cr, request.uid) or {} + self.context['uid'] = self.uid + self._fix_lang(self.context) + return self.context + + def _fix_lang(self, context): + """ OpenERP provides languages which may not make sense and/or may not + be understood by the web client's libraries. + + Fix those here. + + :param dict context: context to fix + """ + lang = context['lang'] + + # inane OpenERP locale + if lang == 'ar_AR': + lang = 'ar' + + # lang to lang_REGION (datejs only handles lang_REGION, no bare langs) + if lang in babel.core.LOCALE_ALIASES: + lang = babel.core.LOCALE_ALIASES[lang] + + context['lang'] = lang or 'en_US' + + """ + Damn properties for retro-compatibility. All of that is deprecated, all + of that. + """ + @property + def _db(self): + return self.db + @_db.setter + def _db(self, value): + self.db = value + @property + def _uid(self): + return self.uid + @_uid.setter + def _uid(self, value): + self.uid = value + @property + def _login(self): + return self.login + @_login.setter + def _login(self, value): + self.login = value + @property + def _password(self): + return self.password + @_password.setter + def _password(self, value): + self.password = value + + def send(self, service_name, method, *args): + """ + .. deprecated:: 8.0 + Use ``openerp.netsvc.dispatch_rpc()`` instead. + """ + return openerp.netsvc.dispatch_rpc(service_name, method, args) + + def proxy(self, service): + """ + .. deprecated:: 8.0 + Use ``openerp.netsvc.dispatch_rpc()`` instead. + """ + return Service(self, service) + + def assert_valid(self, force=False): + """ + .. deprecated:: 8.0 + Use ``check_security()`` instead. + + Ensures this session is valid (logged into the openerp server) + """ + if self.uid and not force: + return + # TODO use authenticate instead of login + self.uid = self.proxy("common").login(self.db, self.login, self.password) + if not self.uid: + raise AuthenticationError("Authentication failure") + + def ensure_valid(self): + """ + .. deprecated:: 8.0 + Use ``check_security()`` instead. + """ + if self.uid: + try: + self.assert_valid(True) + except Exception: + self.uid = None + + def execute(self, model, func, *l, **d): + """ + .. deprecated:: 8.0 + Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. + """ + model = self.model(model) + r = getattr(model, func)(*l, **d) + return r + + def exec_workflow(self, model, id, signal): + """ + .. deprecated:: 8.0 + Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. + """ + self.assert_valid() + r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id) + return r + + def model(self, model): + """ + .. deprecated:: 8.0 + Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. + + Get an RPC proxy for the object ``model``, bound to this session. + + :param model: an OpenERP model name + :type model: str + :rtype: a model object + """ + if not self.db: + raise SessionExpiredException("Session expired") + + return Model(self, model) + +def session_gc(session_store): + if random.random() < 0.001: + # we keep session one week + last_week = time.time() - 60*60*24*7 + for fname in os.listdir(session_store.path): + path = os.path.join(session_store.path, fname) + try: + if os.path.getmtime(path) < last_week: + os.unlink(path) + except OSError: + pass + +#---------------------------------------------------------- +# WSGI Application +#---------------------------------------------------------- +# Add potentially missing (older ubuntu) font mime types +mimetypes.add_type('application/font-woff', '.woff') +mimetypes.add_type('application/vnd.ms-fontobject', '.eot') +mimetypes.add_type('application/x-font-ttf', '.ttf') + +class DisableCacheMiddleware(object): + def __init__(self, app): + self.app = app + def __call__(self, environ, start_response): + def start_wrapped(status, headers): + referer = environ.get('HTTP_REFERER', '') + parsed = urlparse.urlparse(referer) + debug = parsed.query.count('debug') >= 1 + + new_headers = [] + unwanted_keys = ['Last-Modified'] + if debug: + new_headers = [('Cache-Control', 'no-cache')] + unwanted_keys += ['Expires', 'Etag', 'Cache-Control'] + + for k, v in headers: + if k not in unwanted_keys: + new_headers.append((k, v)) + + start_response(status, new_headers) + return self.app(environ, start_wrapped) + +def session_path(): + try: + import pwd + username = pwd.getpwuid(os.geteuid()).pw_name + except ImportError: + try: + username = getpass.getuser() + except Exception: + username = "unknown" + path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) + try: + os.mkdir(path, 0700) + except OSError as exc: + if exc.errno == errno.EEXIST: + # directory exists: ensure it has the correct permissions + # this will fail if the directory is not owned by the current user + os.chmod(path, 0700) + else: + raise + return path + +class Root(object): + """Root WSGI application for the OpenERP Web Client. + """ + def __init__(self): + self.addons = {} + self.statics = {} + + self.no_db_router = None + + self.load_addons() + + # Setup http sessions + path = session_path() + self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) + _logger.debug('HTTP sessions stored in: %s', path) + + + def __call__(self, environ, start_response): + """ Handle a WSGI request + """ + return self.dispatch(environ, start_response) + + def dispatch(self, environ, start_response): + """ + Performs the actual WSGI dispatching for the application. + """ + try: + httprequest = werkzeug.wrappers.Request(environ) + httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict + httprequest.app = self + + session_gc(self.session_store) + + sid = httprequest.args.get('session_id') + explicit_session = True + if not sid: + sid = httprequest.headers.get("X-Openerp-Session-Id") + if not sid: + sid = httprequest.cookies.get('session_id') + explicit_session = False + if sid is None: + httprequest.session = self.session_store.new() + else: + httprequest.session = self.session_store.get(sid) + + self._find_db(httprequest) + + if not "lang" in httprequest.session.context: + lang = httprequest.accept_languages.best or "en_US" + lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_') + httprequest.session.context["lang"] = lang + + request = self._build_request(httprequest) + db = request.db + + if db: + openerp.modules.registry.RegistryManager.check_registry_signaling(db) + + with set_request(request): + self.find_handler() + result = request.dispatch() + + if db: + openerp.modules.registry.RegistryManager.signal_caches_change(db) + + if isinstance(result, basestring): + headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))] + response = werkzeug.wrappers.Response(result, headers=headers) + else: + response = result + + if httprequest.session.should_save: + self.session_store.save(httprequest.session) + if not explicit_session and hasattr(response, 'set_cookie'): + response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60) + + return response(environ, start_response) + except werkzeug.exceptions.HTTPException, e: + return e(environ, start_response) + + def _find_db(self, httprequest): + db = db_monodb(httprequest) + if db != httprequest.session.db: + httprequest.session.logout() + httprequest.session.db = db + + def _build_request(self, httprequest): + if httprequest.args.get('jsonp'): + return JsonRequest(httprequest) + + if httprequest.mimetype == "application/json": + return JsonRequest(httprequest) + else: + return HttpRequest(httprequest) + + def load_addons(self): + """ Load all addons from addons patch containg static files and + controllers and configure them. """ + + for addons_path in openerp.modules.module.ad_paths: + for module in sorted(os.listdir(str(addons_path))): + if module not in addons_module: + manifest_path = os.path.join(addons_path, module, '__openerp__.py') + 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()) + manifest['addons_path'] = addons_path + _logger.debug("Loading %s", module) + if 'openerp.addons' in sys.modules: + m = __import__('openerp.addons.' + module) + else: + m = __import__(module) + addons_module[module] = m + addons_manifest[module] = manifest + self.statics['/%s/static' % module] = path_static + + app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics) + self.dispatch = DisableCacheMiddleware(app) + + def _build_router(self, db): + _logger.info("Generating routing configuration for database %s" % db) + routing_map = routing.Map(strict_slashes=False) + + def gen(modules, nodb_only): + for module in modules: + for v in controllers_per_module[module]: + cls = v[1] + + subclasses = cls.__subclasses__() + subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and + c.__module__.split(".")[2] in modules] + if subclasses: + name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses)) + cls = type(name, tuple(reversed(subclasses)), {}) + + o = cls() + members = inspect.getmembers(o) + for mk, mv in members: + if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \ + nodb_only == (getattr(mv, "auth", "none") == "none"): + for url in mv.routes: + if getattr(mv, "combine", False): + url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') + if url.endswith("/") and len(url) > 1: + url = url[: -1] + routing_map.add(routing.Rule(url, endpoint=mv)) + + modules_set = set(controllers_per_module.keys()) - set(['', 'web']) + # building all none methods + gen(['', "web"] + sorted(modules_set), True) + if not db: + return routing_map + + registry = openerp.modules.registry.RegistryManager.get(db) + with registry.cursor() as cr: + m = registry.get('ir.module.module') + ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')]) + installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'])) + modules_set = modules_set & installed + + # building all other methods + gen(['', "web"] + sorted(modules_set), False) + + return routing_map + + def get_db_router(self, db): + if db is None: + router = self.no_db_router + else: + router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None) + if not router: + router = self._build_router(db) + if db is None: + self.no_db_router = router + else: + openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router + return router + + def find_handler(self): + """ + Tries to discover the controller handling the request for the path specified in the request. + """ + path = request.httprequest.path + urls = self.get_db_router(request.db).bind("") + func, arguments = urls.match(path) + arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")]) + + @service_model.check + def checked_call(dbname, *a, **kw): + return func(*a, **kw) + + def nfunc(*args, **kwargs): + kwargs.update(arguments) + if getattr(func, '_first_arg_is_req', False): + args = (request,) + args + + if request.db: + return checked_call(request.db, *args, **kwargs) + return func(*args, **kwargs) + + request.func = nfunc + request.auth_method = getattr(func, "auth", "user") + request.func_request_type = func.exposed + +def db_list(force=False, httprequest=None): + httprequest = httprequest or request.httprequest + dbs = openerp.netsvc.dispatch_rpc("db", "list", [force]) + h = httprequest.environ['HTTP_HOST'].split(':')[0] + d = h.split('.')[0] + r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d) + dbs = [i for i in dbs if re.match(r, i)] + return dbs + +def db_monodb(httprequest=None): + """ + Magic function to find the current database. + + Implementation details: + + * Magic + * More magic + + Returns ``None`` if the magic is not magic enough. + """ + httprequest = httprequest or request.httprequest + db = None + redirect = None + + dbs = db_list(True, httprequest) + + # try the db already in the session + db_session = httprequest.session.db + if db_session in dbs: + return db_session + + # if dbfilters was specified when launching the server and there is + # only one possible db, we take that one + if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1: + return dbs[0] + return None + +class CommonController(Controller): + + @route('/jsonrpc', type='json', auth="none") + def jsonrpc(self, service, method, args): + """ Method used by client APIs to contact OpenERP. """ + return openerp.netsvc.dispatch_rpc(service, method, args) + + @route('/gen_session_id', type='json', auth="none") + def gen_session_id(self): + nsession = root.session_store.new() + return nsession.sid + +root = None + +def wsgi_postload(): + global root + root = Root() + openerp.wsgi.register_wsgi_handler(root) + +# vim:et:ts=4:sw=4: From 401099d35841bc347559eca86c195464d857f5d9 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 27 Oct 2013 18:17:17 +0100 Subject: [PATCH 6/8] [IMP] move http layer from openerp-web to openobject-server, convert controllers to the new route api bzr revid: al@openerp.com-20131027171717-dzqx2264p152lq8y --- addons/web/__init__.py | 8 +- addons/web/controllers/main.py | 5 +- addons/web/controllers/testing.py | 7 +- addons/web/doc/web_controllers.rst | 4 +- addons/web/http.py | 1116 ------------------------ addons/web/tests/common.py | 2 +- addons/web/tests/test_dataset.py | 2 +- addons/web/tests/test_menu.py | 2 +- addons/web_diagram/controllers/main.py | 4 +- 9 files changed, 19 insertions(+), 1131 deletions(-) delete mode 100644 addons/web/http.py diff --git a/addons/web/__init__.py b/addons/web/__init__.py index e01969ddcca..5a0289cef6d 100644 --- a/addons/web/__init__.py +++ b/addons/web/__init__.py @@ -1,4 +1,10 @@ -import http +import sys + +# Mock deprecated openerp.addons.web.http module +import openerp.http +sys.modules['openerp.addons.web.http'] = openerp.http +http = openerp.http + import controllers import cli diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py index 7a52f289f8f..4c95f4d5f32 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -33,10 +33,9 @@ import openerp import openerp.modules.registry from openerp.tools.translate import _ from openerp.tools import config +from openerp import http -from .. import http - -from openerp.addons.web.http import request +from openerp.http import request #---------------------------------------------------------- # OpenERP Web helpers diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py index b51b12fc53d..0160f0f28cd 100644 --- a/addons/web/controllers/testing.py +++ b/addons/web/controllers/testing.py @@ -9,11 +9,10 @@ import os from mako.template import Template from openerp.modules import module +from openerp import http +from openerp.http import request from .main import module_topological_sort -from .. import http - -from ..http import request NOMODULE_TEMPLATE = Template(u""" @@ -171,4 +170,4 @@ class TestRunnerController(http.Controller): @http.route('/web/tests/get_session_value', type='json', auth="none") def get_session_value(self): - return request.session.some_test_value \ No newline at end of file + return request.session.some_test_value diff --git a/addons/web/doc/web_controllers.rst b/addons/web/doc/web_controllers.rst index 71ff9fb8416..442d2b0caa7 100644 --- a/addons/web/doc/web_controllers.rst +++ b/addons/web/doc/web_controllers.rst @@ -35,8 +35,8 @@ Now you can put the following content in ``controllers/my_controllers.py``: :: - import openerp.addons.web.http as http - from openerp.addons.web.http import request + import openerp.http as http + from openerp.http import request Controller Declaration diff --git a/addons/web/http.py b/addons/web/http.py deleted file mode 100644 index 7bad66cb7c4..00000000000 --- a/addons/web/http.py +++ /dev/null @@ -1,1116 +0,0 @@ -# -*- coding: utf-8 -*- -#---------------------------------------------------------- -# OpenERP Web HTTP layer -#---------------------------------------------------------- -import ast -import cgi -import contextlib -import functools -import getpass -import logging -import mimetypes -import os -import pprint -import random -import sys -import tempfile -import threading -import time -import traceback -import urlparse -import uuid -import errno -import re -import warnings - -import babel.core -import simplejson -import werkzeug.contrib.sessions -import werkzeug.datastructures -import werkzeug.exceptions -import werkzeug.utils -import werkzeug.wrappers -import werkzeug.wsgi -import werkzeug.routing as routing -import urllib -import urllib2 - -import openerp -from openerp.service import security, model as service_model -from openerp.tools import config - -import inspect -import functools - -_logger = logging.getLogger(__name__) - -#---------------------------------------------------------- -# RequestHandler -#---------------------------------------------------------- -class WebRequest(object): - """ Parent class for all OpenERP Web request types, mostly deals with - initialization and setup of the request object (the dispatching itself has - to be handled by the subclasses) - - :param request: a wrapped werkzeug Request object - :type request: :class:`werkzeug.wrappers.BaseRequest` - - .. attribute:: httprequest - - the original :class:`werkzeug.wrappers.Request` object provided to the - request - - .. attribute:: httpsession - - .. deprecated:: 8.0 - - Use ``self.session`` instead. - - .. attribute:: params - - :class:`~collections.Mapping` of request parameters, not generally - useful as they're provided directly to the handler method as keyword - arguments - - .. attribute:: session_id - - opaque identifier for the :class:`session.OpenERPSession` instance of - the current request - - .. attribute:: session - - a :class:`OpenERPSession` holding the HTTP session data for the - current http session - - .. attribute:: context - - :class:`~collections.Mapping` of context values for the current request - - .. attribute:: db - - ``str``, the name of the database linked to the current request. Can be ``None`` - if the current request uses the ``none`` authentication. - - .. attribute:: uid - - ``int``, the id of the user related to the current request. Can be ``None`` - if the current request uses the ``none`` authenticatoin. - """ - def __init__(self, httprequest): - self.httprequest = httprequest - self.httpresponse = None - self.httpsession = httprequest.session - self.session = httprequest.session - self.session_id = httprequest.session.sid - self.disable_db = False - self.uid = None - self.func = None - self.auth_method = None - self._cr_cm = None - self._cr = None - self.func_request_type = None - # set db/uid trackers - they're cleaned up at the WSGI - # dispatching phase in openerp.service.wsgi_server.application - if self.db: - threading.current_thread().dbname = self.db - if self.session.uid: - threading.current_thread().uid = self.session.uid - self.context = dict(self.session.context) - self.lang = self.context["lang"] - - def _authenticate(self): - if self.session.uid: - try: - self.session.check_security() - except SessionExpiredException, e: - self.session.logout() - raise SessionExpiredException("Session expired for request %s" % self.httprequest) - auth_methods[self.auth_method]() - @property - def registry(self): - """ - The registry to the database linked to this request. Can be ``None`` if the current request uses the - ``none'' authentication. - """ - return openerp.modules.registry.RegistryManager.get(self.db) if self.db else None - - @property - def db(self): - """ - The registry to the database linked to this request. Can be ``None`` if the current request uses the - ``none'' authentication. - """ - return self.session.db if not self.disable_db else None - - @property - def cr(self): - """ - The cursor initialized for the current method call. If the current request uses the ``none`` authentication - trying to access this property will raise an exception. - """ - # some magic to lazy create the cr - if not self._cr_cm: - self._cr_cm = self.registry.cursor() - self._cr = self._cr_cm.__enter__() - return self._cr - - def _call_function(self, *args, **kwargs): - self._authenticate() - try: - # ugly syntax only to get the __exit__ arguments to pass to self._cr - request = self - class with_obj(object): - def __enter__(self): - pass - def __exit__(self, *args): - if request._cr_cm: - request._cr_cm.__exit__(*args) - request._cr_cm = None - request._cr = None - - with with_obj(): - if self.func_request_type != self._request_type: - raise Exception("%s, %s: Function declared as capable of handling request of type '%s' but called with a request of type '%s'" \ - % (self.func, self.httprequest.path, self.func_request_type, self._request_type)) - return self.func(*args, **kwargs) - finally: - # just to be sure no one tries to re-use the request - self.disable_db = True - self.uid = None - - @property - def debug(self): - return 'debug' in self.httprequest.args - - @contextlib.contextmanager - def registry_cr(self): - warnings.warn('please use request.registry and request.cr directly', DeprecationWarning) - yield (self.registry, self.cr) - -def auth_method_user(): - request.uid = request.session.uid - if not request.uid: - raise SessionExpiredException("Session expired") - -def auth_method_admin(): - if not request.db: - raise SessionExpiredException("No valid database for request %s" % request.httprequest) - request.uid = openerp.SUPERUSER_ID - -def auth_method_none(): - request.disable_db = True - request.uid = None - -auth_methods = { - "user": auth_method_user, - "admin": auth_method_admin, - "none": auth_method_none, -} - -def route(route, type="http", auth="user"): - """ - Decorator marking the decorated method as being a handler for requests. The method must be part of a subclass - of ``Controller``. - - :param route: string or array. The route part that will determine which http requests will match the decorated - method. Can be a single string or an array of strings. See werkzeug's routing documentation for the format of - route expression ( http://werkzeug.pocoo.org/docs/routing/ ). - :param type: The type of request, can be ``'http'`` or ``'json'``. - :param auth: The type of authentication method, can on of the following: - - * ``user``: The user must be authenticated and the current request will perform using the rights of the - user. - * ``admin``: The user may not be authenticated and the current request will perform using the admin user. - * ``none``: The method is always active, even if there is no database. Mainly used by the framework and - authentication modules. There request code will not have any facilities to access the database nor have any - configuration indicating the current database nor the current user. - """ - assert type in ["http", "json"] - assert auth in auth_methods.keys() - def decorator(f): - if isinstance(route, list): - f.routes = route - else: - f.routes = [route] - f.exposed = type - if getattr(f, "auth", None) is None: - f.auth = auth - return f - return decorator - -def reject_nonliteral(dct): - if '__ref' in dct: - raise ValueError( - "Non literal contexts can not be sent to the server anymore (%r)" % (dct,)) - return dct - -class JsonRequest(WebRequest): - """ JSON-RPC2 over HTTP. - - Sucessful request:: - - --> {"jsonrpc": "2.0", - "method": "call", - "params": {"context": {}, - "arg1": "val1" }, - "id": null} - - <-- {"jsonrpc": "2.0", - "result": { "res1": "val1" }, - "id": null} - - Request producing a error:: - - --> {"jsonrpc": "2.0", - "method": "call", - "params": {"context": {}, - "arg1": "val1" }, - "id": null} - - <-- {"jsonrpc": "2.0", - "error": {"code": 1, - "message": "End user error message.", - "data": {"code": "codestring", - "debug": "traceback" } }, - "id": null} - - """ - _request_type = "json" - - def __init__(self, *args): - super(JsonRequest, self).__init__(*args) - - self.jsonp_handler = None - - args = self.httprequest.args - jsonp = args.get('jsonp') - self.jsonp = jsonp - request = None - request_id = args.get('id') - - if jsonp and self.httprequest.method == 'POST': - # jsonp 2 steps step1 POST: save call - def handler(): - self.session.jsonp_requests[request_id] = self.httprequest.form['r'] - self.session.modified = True - headers=[('Content-Type', 'text/plain; charset=utf-8')] - r = werkzeug.wrappers.Response(request_id, headers=headers) - return r - self.jsonp_handler = handler - return - elif jsonp and args.get('r'): - # jsonp method GET - request = args.get('r') - elif jsonp and request_id: - # jsonp 2 steps step2 GET: run and return result - request = self.session.jsonp_requests.pop(request_id, "") - else: - # regular jsonrpc2 - request = self.httprequest.stream.read() - - # Read POST content or POST Form Data named "request" - self.jsonrequest = simplejson.loads(request, object_hook=reject_nonliteral) - self.params = dict(self.jsonrequest.get("params", {})) - self.context = self.params.pop('context', self.session.context) - - def dispatch(self): - """ Calls the method asked for by the JSON-RPC2 or JSONP request - """ - if self.jsonp_handler: - return self.jsonp_handler() - response = {"jsonrpc": "2.0" } - error = None - - try: - response['id'] = self.jsonrequest.get('id') - response["result"] = self._call_function(**self.params) - except AuthenticationError, e: - _logger.exception("Exception during JSON request handling.") - se = serialize_exception(e) - error = { - 'code': 100, - 'message': "OpenERP Session Invalid", - 'data': se - } - except Exception, e: - _logger.exception("Exception during JSON request handling.") - se = serialize_exception(e) - error = { - 'code': 200, - 'message': "OpenERP Server Error", - 'data': se - } - if error: - response["error"] = error - - if self.jsonp: - # If we use jsonp, that's mean we are called from another host - # Some browser (IE and Safari) do no allow third party cookies - # We need then to manage http sessions manually. - response['session_id'] = self.session_id - mime = 'application/javascript' - body = "%s(%s);" % (self.jsonp, simplejson.dumps(response),) - else: - mime = 'application/json' - body = simplejson.dumps(response) - - r = werkzeug.wrappers.Response(body, headers=[('Content-Type', mime), ('Content-Length', len(body))]) - return r - -def serialize_exception(e): - tmp = { - "name": type(e).__module__ + "." + type(e).__name__ if type(e).__module__ else type(e).__name__, - "debug": traceback.format_exc(), - "message": u"%s" % e, - "arguments": to_jsonable(e.args), - } - if isinstance(e, openerp.osv.osv.except_osv): - tmp["exception_type"] = "except_osv" - elif isinstance(e, openerp.exceptions.Warning): - tmp["exception_type"] = "warning" - elif isinstance(e, openerp.exceptions.AccessError): - tmp["exception_type"] = "access_error" - elif isinstance(e, openerp.exceptions.AccessDenied): - tmp["exception_type"] = "access_denied" - return tmp - -def to_jsonable(o): - if isinstance(o, str) or isinstance(o,unicode) or isinstance(o, int) or isinstance(o, long) \ - or isinstance(o, bool) or o is None or isinstance(o, float): - return o - if isinstance(o, list) or isinstance(o, tuple): - return [to_jsonable(x) for x in o] - if isinstance(o, dict): - tmp = {} - for k, v in o.items(): - tmp[u"%s" % k] = to_jsonable(v) - return tmp - return u"%s" % o - -def jsonrequest(f): - """ - .. deprecated:: 8.0 - - Use the ``route()`` decorator instead. - """ - f.combine = True - base = f.__name__.lstrip('/') - if f.__name__ == "index": - base = "" - return route([base, base + "/"], type="json", auth="user")(f) - -class HttpRequest(WebRequest): - """ Regular GET/POST request - """ - _request_type = "http" - - def __init__(self, *args): - super(HttpRequest, self).__init__(*args) - params = dict(self.httprequest.args) - params.update(self.httprequest.form) - params.update(self.httprequest.files) - params.pop('session_id', None) - self.params = params - - def dispatch(self): - try: - r = self._call_function(**self.params) - except werkzeug.exceptions.HTTPException, e: - r = e - except Exception, e: - _logger.exception("An exception occured during an http request") - se = serialize_exception(e) - error = { - 'code': 200, - 'message': "OpenERP Server Error", - 'data': se - } - r = werkzeug.exceptions.InternalServerError(cgi.escape(simplejson.dumps(error))) - else: - if not r: - r = werkzeug.wrappers.Response(status=204) # no content - return r - - def make_response(self, data, headers=None, cookies=None): - """ Helper for non-HTML responses, or HTML responses with custom - response headers or cookies. - - While handlers can just return the HTML markup of a page they want to - send as a string if non-HTML data is returned they need to create a - complete response object, or the returned data will not be correctly - interpreted by the clients. - - :param basestring data: response body - :param headers: HTTP headers to set on the response - :type headers: ``[(name, value)]`` - :param collections.Mapping cookies: cookies to set on the client - """ - response = werkzeug.wrappers.Response(data, headers=headers) - if cookies: - for k, v in cookies.iteritems(): - response.set_cookie(k, v) - return response - - def not_found(self, description=None): - """ Helper for 404 response, return its result from the method - """ - return werkzeug.exceptions.NotFound(description) - -def httprequest(f): - """ - .. deprecated:: 8.0 - - Use the ``route()`` decorator instead. - """ - f.combine = True - base = f.__name__.lstrip('/') - if f.__name__ == "index": - base = "" - return route([base, base + "/"], type="http", auth="user")(f) - -#---------------------------------------------------------- -# Local storage of requests -#---------------------------------------------------------- -from werkzeug.local import LocalStack - -_request_stack = LocalStack() - - -@contextlib.contextmanager -def set_request(req): - _request_stack.push(req) - try: - yield - finally: - _request_stack.pop() - - -request = _request_stack() -""" - A global proxy that always redirect to the current request object. -""" - -#---------------------------------------------------------- -# Controller registration with a metaclass -#---------------------------------------------------------- -addons_module = {} -addons_manifest = {} -controllers_per_module = {} - -class ControllerType(type): - def __init__(cls, name, bases, attrs): - super(ControllerType, cls).__init__(name, bases, attrs) - - # flag old-style methods with req as first argument - for k, v in attrs.items(): - if inspect.isfunction(v): - spec = inspect.getargspec(v) - first_arg = spec.args[1] if len(spec.args) >= 2 else None - if first_arg in ["req", "request"]: - v._first_arg_is_req = True - - # store the controller in the controllers list - name_class = ("%s.%s" % (cls.__module__, cls.__name__), cls) - class_path = name_class[0].split(".") - if not class_path[:2] == ["openerp", "addons"]: - return - # we want to know all modules that have controllers - module = class_path[2] - # but we only store controllers directly inheriting from Controller - if not "Controller" in globals() or not Controller in bases: - return - controllers_per_module.setdefault(module, []).append(name_class) - -class Controller(object): - __metaclass__ = ControllerType - -############################# -# OpenERP Sessions # -############################# - -class AuthenticationError(Exception): - pass - -class SessionExpiredException(Exception): - pass - -class Service(object): - """ - .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. - """ - def __init__(self, session, service_name): - self.session = session - self.service_name = service_name - - def __getattr__(self, method): - def proxy_method(*args): - result = openerp.netsvc.dispatch_rpc(self.service_name, method, args) - return result - return proxy_method - -class Model(object): - """ - .. deprecated:: 8.0 - Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. - """ - def __init__(self, session, model): - self.session = session - self.model = model - self.proxy = self.session.proxy('object') - - def __getattr__(self, method): - self.session.assert_valid() - def proxy(*args, **kw): - # Can't provide any retro-compatibility for this case, so we check it and raise an Exception - # to tell the programmer to adapt his code - if not request.db or not request.uid or self.session.db != request.db \ - or self.session.uid != request.uid: - raise Exception("Trying to use Model with badly configured database or user.") - - mod = request.registry.get(self.model) - if method.startswith('_'): - raise Exception("Access denied") - meth = getattr(mod, method) - cr = request.cr - result = meth(cr, request.uid, *args, **kw) - # reorder read - if method == "read": - if isinstance(result, list) and len(result) > 0 and "id" in result[0]: - index = {} - for r in result: - index[r['id']] = r - result = [index[x] for x in args[0] if x in index] - return result - return proxy - -class OpenERPSession(werkzeug.contrib.sessions.Session): - def __init__(self, *args, **kwargs): - self.inited = False - self.modified = False - super(OpenERPSession, self).__init__(*args, **kwargs) - self.inited = True - self._default_values() - self.modified = False - - def __getattr__(self, attr): - return self.get(attr, None) - def __setattr__(self, k, v): - if getattr(self, "inited", False): - try: - object.__getattribute__(self, k) - except: - return self.__setitem__(k, v) - object.__setattr__(self, k, v) - - def authenticate(self, db, login=None, password=None, uid=None): - """ - Authenticate the current user with the given db, login and password. If successful, store - the authentication parameters in the current session and request. - - :param uid: If not None, that user id will be used instead the login to authenticate the user. - """ - - if uid is None: - wsgienv = request.httprequest.environ - env = dict( - base_location=request.httprequest.url_root.rstrip('/'), - HTTP_HOST=wsgienv['HTTP_HOST'], - REMOTE_ADDR=wsgienv['REMOTE_ADDR'], - ) - uid = openerp.netsvc.dispatch_rpc('common', 'authenticate', [db, login, password, env]) - else: - security.check(db, uid, password) - self.db = db - self.uid = uid - self.login = login - self.password = password - request.uid = uid - request.disable_db = False - - if uid: self.get_context() - return uid - - def check_security(self): - """ - Chech the current authentication parameters to know if those are still valid. This method - should be called at each request. If the authentication fails, a ``SessionExpiredException`` - is raised. - """ - if not self.db or not self.uid: - raise SessionExpiredException("Session expired") - security.check(self.db, self.uid, self.password) - - def logout(self): - for k in self.keys(): - del self[k] - self._default_values() - - def _default_values(self): - self.setdefault("db", None) - self.setdefault("uid", None) - self.setdefault("login", None) - self.setdefault("password", None) - self.setdefault("context", {'tz': "UTC", "uid": None}) - self.setdefault("jsonp_requests", {}) - - 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 = request.registry.get('res.users').context_get(request.cr, request.uid) or {} - self.context['uid'] = self.uid - self._fix_lang(self.context) - return self.context - - def _fix_lang(self, context): - """ OpenERP provides languages which may not make sense and/or may not - be understood by the web client's libraries. - - Fix those here. - - :param dict context: context to fix - """ - lang = context['lang'] - - # inane OpenERP locale - if lang == 'ar_AR': - lang = 'ar' - - # lang to lang_REGION (datejs only handles lang_REGION, no bare langs) - if lang in babel.core.LOCALE_ALIASES: - lang = babel.core.LOCALE_ALIASES[lang] - - context['lang'] = lang or 'en_US' - - """ - Damn properties for retro-compatibility. All of that is deprecated, all - of that. - """ - @property - def _db(self): - return self.db - @_db.setter - def _db(self, value): - self.db = value - @property - def _uid(self): - return self.uid - @_uid.setter - def _uid(self, value): - self.uid = value - @property - def _login(self): - return self.login - @_login.setter - def _login(self, value): - self.login = value - @property - def _password(self): - return self.password - @_password.setter - def _password(self, value): - self.password = value - - def send(self, service_name, method, *args): - """ - .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. - """ - return openerp.netsvc.dispatch_rpc(service_name, method, args) - - def proxy(self, service): - """ - .. deprecated:: 8.0 - Use ``openerp.netsvc.dispatch_rpc()`` instead. - """ - return Service(self, service) - - def assert_valid(self, force=False): - """ - .. deprecated:: 8.0 - Use ``check_security()`` instead. - - Ensures this session is valid (logged into the openerp server) - """ - if self.uid and not force: - return - # TODO use authenticate instead of login - self.uid = self.proxy("common").login(self.db, self.login, self.password) - if not self.uid: - raise AuthenticationError("Authentication failure") - - def ensure_valid(self): - """ - .. deprecated:: 8.0 - Use ``check_security()`` instead. - """ - if self.uid: - try: - self.assert_valid(True) - except Exception: - self.uid = None - - def execute(self, model, func, *l, **d): - """ - .. deprecated:: 8.0 - Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. - """ - model = self.model(model) - r = getattr(model, func)(*l, **d) - return r - - def exec_workflow(self, model, id, signal): - """ - .. deprecated:: 8.0 - Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. - """ - self.assert_valid() - r = self.proxy('object').exec_workflow(self.db, self.uid, self.password, model, signal, id) - return r - - def model(self, model): - """ - .. deprecated:: 8.0 - Use the resistry and cursor in ``openerp.addons.web.http.request`` instead. - - Get an RPC proxy for the object ``model``, bound to this session. - - :param model: an OpenERP model name - :type model: str - :rtype: a model object - """ - if not self.db: - raise SessionExpiredException("Session expired") - - return Model(self, model) - -def session_gc(session_store): - if random.random() < 0.001: - # we keep session one week - last_week = time.time() - 60*60*24*7 - for fname in os.listdir(session_store.path): - path = os.path.join(session_store.path, fname) - try: - if os.path.getmtime(path) < last_week: - os.unlink(path) - except OSError: - pass - -#---------------------------------------------------------- -# WSGI Application -#---------------------------------------------------------- -# Add potentially missing (older ubuntu) font mime types -mimetypes.add_type('application/font-woff', '.woff') -mimetypes.add_type('application/vnd.ms-fontobject', '.eot') -mimetypes.add_type('application/x-font-ttf', '.ttf') - -class DisableCacheMiddleware(object): - def __init__(self, app): - self.app = app - def __call__(self, environ, start_response): - def start_wrapped(status, headers): - referer = environ.get('HTTP_REFERER', '') - parsed = urlparse.urlparse(referer) - debug = parsed.query.count('debug') >= 1 - - new_headers = [] - unwanted_keys = ['Last-Modified'] - if debug: - new_headers = [('Cache-Control', 'no-cache')] - unwanted_keys += ['Expires', 'Etag', 'Cache-Control'] - - for k, v in headers: - if k not in unwanted_keys: - new_headers.append((k, v)) - - start_response(status, new_headers) - return self.app(environ, start_wrapped) - -def session_path(): - try: - import pwd - username = pwd.getpwuid(os.geteuid()).pw_name - except ImportError: - try: - username = getpass.getuser() - except Exception: - username = "unknown" - path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) - try: - os.mkdir(path, 0700) - except OSError as exc: - if exc.errno == errno.EEXIST: - # directory exists: ensure it has the correct permissions - # this will fail if the directory is not owned by the current user - os.chmod(path, 0700) - else: - raise - return path - -class Root(object): - """Root WSGI application for the OpenERP Web Client. - """ - def __init__(self): - self.addons = {} - self.statics = {} - - self.no_db_router = None - - self.load_addons() - - # Setup http sessions - path = session_path() - self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path, session_class=OpenERPSession) - _logger.debug('HTTP sessions stored in: %s', path) - - - def __call__(self, environ, start_response): - """ Handle a WSGI request - """ - return self.dispatch(environ, start_response) - - def dispatch(self, environ, start_response): - """ - Performs the actual WSGI dispatching for the application. - """ - try: - httprequest = werkzeug.wrappers.Request(environ) - httprequest.parameter_storage_class = werkzeug.datastructures.ImmutableDict - httprequest.app = self - - session_gc(self.session_store) - - sid = httprequest.args.get('session_id') - explicit_session = True - if not sid: - sid = httprequest.headers.get("X-Openerp-Session-Id") - if not sid: - sid = httprequest.cookies.get('session_id') - explicit_session = False - if sid is None: - httprequest.session = self.session_store.new() - else: - httprequest.session = self.session_store.get(sid) - - self._find_db(httprequest) - - if not "lang" in httprequest.session.context: - lang = httprequest.accept_languages.best or "en_US" - lang = babel.core.LOCALE_ALIASES.get(lang, lang).replace('-', '_') - httprequest.session.context["lang"] = lang - - request = self._build_request(httprequest) - db = request.db - - if db: - openerp.modules.registry.RegistryManager.check_registry_signaling(db) - - with set_request(request): - self.find_handler() - result = request.dispatch() - - if db: - openerp.modules.registry.RegistryManager.signal_caches_change(db) - - if isinstance(result, basestring): - headers=[('Content-Type', 'text/html; charset=utf-8'), ('Content-Length', len(result))] - response = werkzeug.wrappers.Response(result, headers=headers) - else: - response = result - - if httprequest.session.should_save: - self.session_store.save(httprequest.session) - if not explicit_session and hasattr(response, 'set_cookie'): - response.set_cookie('session_id', httprequest.session.sid, max_age=90 * 24 * 60 * 60) - - return response(environ, start_response) - except werkzeug.exceptions.HTTPException, e: - return e(environ, start_response) - - def _find_db(self, httprequest): - db = db_monodb(httprequest) - if db != httprequest.session.db: - httprequest.session.logout() - httprequest.session.db = db - - def _build_request(self, httprequest): - if httprequest.args.get('jsonp'): - return JsonRequest(httprequest) - - if httprequest.mimetype == "application/json": - return JsonRequest(httprequest) - else: - return HttpRequest(httprequest) - - def load_addons(self): - """ Load all addons from addons patch containg static files and - controllers and configure them. """ - - for addons_path in openerp.modules.module.ad_paths: - for module in sorted(os.listdir(str(addons_path))): - if module not in addons_module: - manifest_path = os.path.join(addons_path, module, '__openerp__.py') - 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()) - manifest['addons_path'] = addons_path - _logger.debug("Loading %s", module) - if 'openerp.addons' in sys.modules: - m = __import__('openerp.addons.' + module) - else: - m = __import__(module) - addons_module[module] = m - addons_manifest[module] = manifest - self.statics['/%s/static' % module] = path_static - - app = werkzeug.wsgi.SharedDataMiddleware(self.dispatch, self.statics) - self.dispatch = DisableCacheMiddleware(app) - - def _build_router(self, db): - _logger.info("Generating routing configuration for database %s" % db) - routing_map = routing.Map(strict_slashes=False) - - def gen(modules, nodb_only): - for module in modules: - for v in controllers_per_module[module]: - cls = v[1] - - subclasses = cls.__subclasses__() - subclasses = [c for c in subclasses if c.__module__.startswith('openerp.addons.') and - c.__module__.split(".")[2] in modules] - if subclasses: - name = "%s (extended by %s)" % (cls.__name__, ', '.join(sub.__name__ for sub in subclasses)) - cls = type(name, tuple(reversed(subclasses)), {}) - - o = cls() - members = inspect.getmembers(o) - for mk, mv in members: - if inspect.ismethod(mv) and getattr(mv, 'exposed', False) and \ - nodb_only == (getattr(mv, "auth", "none") == "none"): - for url in mv.routes: - if getattr(mv, "combine", False): - url = o._cp_path.rstrip('/') + '/' + url.lstrip('/') - if url.endswith("/") and len(url) > 1: - url = url[: -1] - routing_map.add(routing.Rule(url, endpoint=mv)) - - modules_set = set(controllers_per_module.keys()) - set(['web']) - # building all none methods - gen(["web"] + sorted(modules_set), True) - if not db: - return routing_map - - registry = openerp.modules.registry.RegistryManager.get(db) - with registry.cursor() as cr: - m = registry.get('ir.module.module') - ids = m.search(cr, openerp.SUPERUSER_ID, [('state', '=', 'installed'), ('name', '!=', 'web')]) - installed = set(x['name'] for x in m.read(cr, 1, ids, ['name'])) - modules_set = modules_set & installed - - # building all other methods - gen(["web"] + sorted(modules_set), False) - - return routing_map - - def get_db_router(self, db): - if db is None: - router = self.no_db_router - else: - router = getattr(openerp.modules.registry.RegistryManager.get(db), "werkzeug_http_router", None) - if not router: - router = self._build_router(db) - if db is None: - self.no_db_router = router - else: - openerp.modules.registry.RegistryManager.get(db).werkzeug_http_router = router - return router - - def find_handler(self): - """ - Tries to discover the controller handling the request for the path specified in the request. - """ - path = request.httprequest.path - urls = self.get_db_router(request.db).bind("") - func, arguments = urls.match(path) - arguments = dict([(k, v) for k, v in arguments.items() if not k.startswith("_ignored_")]) - - @service_model.check - def checked_call(dbname, *a, **kw): - return func(*a, **kw) - - def nfunc(*args, **kwargs): - kwargs.update(arguments) - if getattr(func, '_first_arg_is_req', False): - args = (request,) + args - - if request.db: - return checked_call(request.db, *args, **kwargs) - return func(*args, **kwargs) - - request.func = nfunc - request.auth_method = getattr(func, "auth", "user") - request.func_request_type = func.exposed - -root = None - -def db_list(force=False, httprequest=None): - httprequest = httprequest or request.httprequest - dbs = openerp.netsvc.dispatch_rpc("db", "list", [force]) - h = httprequest.environ['HTTP_HOST'].split(':')[0] - d = h.split('.')[0] - r = openerp.tools.config['dbfilter'].replace('%h', h).replace('%d', d) - dbs = [i for i in dbs if re.match(r, i)] - return dbs - -def db_monodb(httprequest=None): - """ - Magic function to find the current database. - - Implementation details: - - * Magic - * More magic - - Returns ``None`` if the magic is not magic enough. - """ - httprequest = httprequest or request.httprequest - db = None - redirect = None - - dbs = db_list(True, httprequest) - - # try the db already in the session - db_session = httprequest.session.db - if db_session in dbs: - return db_session - - # if dbfilters was specified when launching the server and there is - # only one possible db, we take that one - if openerp.tools.config['dbfilter'] != ".*" and len(dbs) == 1: - return dbs[0] - return None - -class CommonController(Controller): - - @route('/jsonrpc', type='json', auth="none") - def jsonrpc(self, service, method, args): - """ Method used by client APIs to contact OpenERP. """ - return openerp.netsvc.dispatch_rpc(service, method, args) - - @route('/gen_session_id', type='json', auth="none") - def gen_session_id(self): - nsession = root.session_store.new() - return nsession.sid - -def wsgi_postload(): - global root - root = Root() - openerp.wsgi.register_wsgi_handler(root) - -# vim:et:ts=4:sw=4: diff --git a/addons/web/tests/common.py b/addons/web/tests/common.py index f95bbbd42e1..ef0ad0987c7 100644 --- a/addons/web/tests/common.py +++ b/addons/web/tests/common.py @@ -3,7 +3,7 @@ import unittest2 import mock -from openerp.addons.web import http +from openerp import http class MockRequestCase(unittest2.TestCase): def setUp(self): diff --git a/addons/web/tests/test_dataset.py b/addons/web/tests/test_dataset.py index f017e5f609a..e89fadb13a7 100644 --- a/addons/web/tests/test_dataset.py +++ b/addons/web/tests/test_dataset.py @@ -2,7 +2,7 @@ from . import common import openerp.addons.web.controllers.main -from openerp.addons.web.http import request as req +from openerp.http import request as req class TestDataSetController(common.MockRequestCase): def setUp(self): diff --git a/addons/web/tests/test_menu.py b/addons/web/tests/test_menu.py index 8e8f3900670..fb9337a589b 100644 --- a/addons/web/tests/test_menu.py +++ b/addons/web/tests/test_menu.py @@ -4,7 +4,7 @@ import collections import mock import unittest2 -from openerp.addons.web.http import request as req +from openerp.http import request as req from . import common diff --git a/addons/web_diagram/controllers/main.py b/addons/web_diagram/controllers/main.py index 5dd625b80c0..d9a435f1924 100644 --- a/addons/web_diagram/controllers/main.py +++ b/addons/web_diagram/controllers/main.py @@ -1,8 +1,8 @@ import openerp -class DiagramView(openerp.addons.web.http.Controller): +class DiagramView(openerp.http.Controller): - @openerp.addons.web.http.route('/web_diagram/diagram/get_diagram_info', type='json', auth='user') + @openerp.http.route('/web_diagram/diagram/get_diagram_info', type='json', auth='user') def get_diagram_info(self, req, id, model, node, connector, src_node, des_node, label, **kw): From d5b4cd627c16930bd6798768ccd77af2bfcb9f87 Mon Sep 17 00:00:00 2001 From: Antony Lesuisse Date: Sun, 27 Oct 2013 18:31:43 +0100 Subject: [PATCH 7/8] more controller conversion to new route api bzr revid: al@openerp.com-20131027173143-tfxc7e7vqn7re6wb --- addons/base_import/controllers.py | 2 +- addons/board/controllers.py | 12 +++++------- addons/web_linkedin/web_linkedin.py | 9 +++------ 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/addons/base_import/controllers.py b/addons/base_import/controllers.py index cb41d27f44a..afaf06e99e0 100644 --- a/addons/base_import/controllers.py +++ b/addons/base_import/controllers.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import simplejson -from openerp.addons.web.http import Controller, route +from openerp.http import Controller, route class ImportController(Controller): @route('/base_import/set_file') diff --git a/addons/board/controllers.py b/addons/board/controllers.py index 3bdebe1ef26..50148977216 100644 --- a/addons/board/controllers.py +++ b/addons/board/controllers.py @@ -4,14 +4,12 @@ from xml.etree import ElementTree import openerp from openerp.addons.web.controllers.main import load_actions_from_ir_values -class Board(openerp.addons.web.http.Controller): - _cp_path = '/board' - - @openerp.addons.web.http.jsonrequest - def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''): +class Board(openerp.http.Controller): + @openerp.http.route('/board/add_to_dashboard', type='json', auth='user') + def add_to_dashboard(self, menu_id, action_id, context_to_save, domain, view_mode, name=''): + req = openerp.http.request # FIXME move this method to board.board model - dashboard_action = load_actions_from_ir_values( - req, 'action', 'tree_but_open', [('ir.ui.menu', menu_id)], False) + dashboard_action = load_actions_from_ir_values('action', 'tree_but_open', [('ir.ui.menu', menu_id)], False) if dashboard_action: action = dashboard_action[0][2] diff --git a/addons/web_linkedin/web_linkedin.py b/addons/web_linkedin/web_linkedin.py index 92adb30d3dd..a07dcd50ea0 100644 --- a/addons/web_linkedin/web_linkedin.py +++ b/addons/web_linkedin/web_linkedin.py @@ -26,13 +26,10 @@ from urlparse import urlparse, urlunparse import openerp from openerp.osv import fields, osv -class Binary(openerp.addons.web.http.Controller): - _cp_path = "/web_linkedin/binary" - - @openerp.addons.web.http.jsonrequest - def url2binary(self, req, url): +class Binary(openerp.http.Controller): + @openerp.http.route('/web_linkedin/binary/url2binary', type='json', auth='user') + def url2binary(self, url): """Used exclusively to load images from LinkedIn profiles, must not be used for anything else.""" - req.session.assert_valid(force=True) _scheme, _netloc, path, params, query, fragment = urlparse(url) # media.linkedin.com is the master domain for LinkedIn media (replicated to CDNs), # so forcing it should always work and prevents abusing this method to load arbitrary URLs From e142005a0169c9faebd585b28ba323b4c8691a79 Mon Sep 17 00:00:00 2001 From: Launchpad Translations on behalf of openerp <> Date: Mon, 28 Oct 2013 05:23:39 +0000 Subject: [PATCH 8/8] Launchpad automatic translations update. bzr revid: launchpad_translations_on_behalf_of_openerp-20131028052339-0v9n7leiirwlacok --- addons/purchase_double_validation/i18n/bs.po | 49 ++ addons/purchase_requisition/i18n/bs.po | 497 +++++++++++++++++++ addons/resource/i18n/bs.po | 364 ++++++++++++++ addons/sale_margin/i18n/bs.po | 48 ++ addons/sale_mrp/i18n/bs.po | 43 ++ addons/sale_order_dates/i18n/bs.po | 58 +++ 6 files changed, 1059 insertions(+) create mode 100644 addons/purchase_double_validation/i18n/bs.po create mode 100644 addons/purchase_requisition/i18n/bs.po create mode 100644 addons/resource/i18n/bs.po create mode 100644 addons/sale_margin/i18n/bs.po create mode 100644 addons/sale_mrp/i18n/bs.po create mode 100644 addons/sale_order_dates/i18n/bs.po diff --git a/addons/purchase_double_validation/i18n/bs.po b/addons/purchase_double_validation/i18n/bs.po new file mode 100644 index 00000000000..fd62a76192a --- /dev/null +++ b/addons/purchase_double_validation/i18n/bs.po @@ -0,0 +1,49 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 14:43+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: purchase_double_validation +#: model:ir.model,name:purchase_double_validation.model_purchase_config_settings +msgid "purchase.config.settings" +msgstr "purchase.config.settings" + +#. module: purchase_double_validation +#: view:purchase.order:0 +msgid "Purchase orders which are not approved yet." +msgstr "Prodajne narudžbe koje još nisu odobrene." + +#. module: purchase_double_validation +#: field:purchase.config.settings,limit_amount:0 +msgid "limit to require a second approval" +msgstr "ograniči da zahtjeva duplo odobrenje" + +#. module: purchase_double_validation +#: view:board.board:0 +#: model:ir.actions.act_window,name:purchase_double_validation.purchase_waiting +msgid "Purchase Orders Waiting Approval" +msgstr "Prodajne narudžbe koje čekaju odobrenje" + +#. module: purchase_double_validation +#: view:purchase.order:0 +msgid "To Approve" +msgstr "Za odobriti" + +#. module: purchase_double_validation +#: help:purchase.config.settings,limit_amount:0 +msgid "Amount after which validation of purchase is required." +msgstr "Iznos nakon kojeg je potrebno odobrenje kupovine" diff --git a/addons/purchase_requisition/i18n/bs.po b/addons/purchase_requisition/i18n/bs.po new file mode 100644 index 00000000000..58fe989b0a1 --- /dev/null +++ b/addons/purchase_requisition/i18n/bs.po @@ -0,0 +1,497 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 14:23+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Request a Quotation" +msgstr "Zahtjevaj predračun" + +#. module: purchase_requisition +#: selection:purchase.requisition,exclusive:0 +msgid "Multiple Requisitions" +msgstr "Višestruki zahtjevi" + +#. module: purchase_requisition +#: field:purchase.requisition.line,product_uom_id:0 +msgid "Product Unit of Measure" +msgstr "Jedinica mjere proizvoda" + +#. module: purchase_requisition +#: model:ir.actions.act_window,help:purchase_requisition.action_purchase_requisition +msgid "" +"

\n" +" Click to start a new purchase requisition process. \n" +"

\n" +" A purchase requisition is the step before a request for " +"quotation.\n" +" In a purchase requisition (or purchase tender), you can record " +"the\n" +" products you need to buy and trigger the creation of RfQs to\n" +" suppliers. After the negotiation, once you have reviewed all " +"the\n" +" supplier's offers, you can validate some and cancel others.\n" +"

\n" +" " +msgstr "" +"

\n" +" Kliknite da započnete novi proces zahtjeva nabavke. \n" +"

\n" +" Zahtjev nabavke je korak prije zahtjeva predračuna. U zahtjevu " +"nabavke\n" +" (ili nabavni tender) možete da zabilježite proizvode koje " +"trebate da\n" +" kupite i okinete kreiranje zahtjeva predračuna dobavljačima. " +"Nakon \n" +" pregovora, kada ste pregledali sve ponude, možete da odobrite " +"neke\n" +" i otkažete druge.\n" +"

\n" +" " + +#. module: purchase_requisition +#: view:purchase.requisition:0 +#: field:purchase.requisition,user_id:0 +msgid "Responsible" +msgstr "Odgovoran" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +#: field:purchase.requisition,state:0 +msgid "Status" +msgstr "Status" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Send to Suppliers" +msgstr "Pošalji dobavljačima" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Group By..." +msgstr "Grupiši po..." + +#. module: purchase_requisition +#: view:purchase.requisition:0 +#: selection:purchase.requisition,state:0 +msgid "Purchase Done" +msgstr "Nabavka završena" + +#. module: purchase_requisition +#: field:purchase.requisition,message_follower_ids:0 +msgid "Followers" +msgstr "Pratioci" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Purchase Requisition in negociation" +msgstr "Nabavni zahtjevi u pregovorima" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +#: field:purchase.requisition.partner,partner_id:0 +msgid "Supplier" +msgstr "Dobavljač" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +#: selection:purchase.requisition,state:0 +msgid "New" +msgstr "Novi" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Product Detail" +msgstr "Detalji proizvoda" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Qty" +msgstr "Kol." + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Type" +msgstr "Tip" + +#. module: purchase_requisition +#: model:ir.actions.act_window,name:purchase_requisition.action_purchase_requisition_partner +#: model:ir.actions.report.xml,name:purchase_requisition.report_purchase_requisition +#: model:ir.model,name:purchase_requisition.model_purchase_requisition +#: model:ir.module.category,name:purchase_requisition.module_category_purchase_requisition +#: field:product.product,purchase_requisition:0 +#: field:purchase.order,requisition_id:0 +#: view:purchase.requisition:0 +#: field:purchase.requisition.line,requisition_id:0 +#: view:purchase.requisition.partner:0 +msgid "Purchase Requisition" +msgstr "Zahtjev nabavke" + +#. module: purchase_requisition +#: model:ir.model,name:purchase_requisition.model_purchase_requisition_line +msgid "Purchase Requisition Line" +msgstr "Stavke zahtjeva nabavke" + +#. module: purchase_requisition +#: view:purchase.order:0 +msgid "Purchase Orders with requisition" +msgstr "Nabavne narudžbe sa zahtjevima" + +#. module: purchase_requisition +#: model:ir.model,name:purchase_requisition.model_product_product +#: field:purchase.requisition.line,product_id:0 +msgid "Product" +msgstr "Proizvod" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Quotations" +msgstr "Predračuni" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Terms and Conditions" +msgstr "Ugovori i Uslovi" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +#: field:purchase.requisition,description:0 +msgid "Description" +msgstr "Opis" + +#. module: purchase_requisition +#: field:purchase.requisition,message_unread:0 +msgid "Unread Messages" +msgstr "Nepročitane poruke" + +#. module: purchase_requisition +#: field:purchase.requisition,company_id:0 +#: field:purchase.requisition.line,company_id:0 +msgid "Company" +msgstr "Kompanija" + +#. module: purchase_requisition +#: view:purchase.requisition.partner:0 +msgid "Create Quotation" +msgstr "Kreiraj predračun" + +#. module: purchase_requisition +#: help:purchase.requisition,message_ids:0 +msgid "Messages and communication history" +msgstr "Poruke i istorija komunikacije" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Approved by Supplier" +msgstr "Odobreno od strane dobavljača" + +#. module: purchase_requisition +#: view:purchase.requisition.partner:0 +msgid "or" +msgstr "ili" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Reset to Draft" +msgstr "Vrati u pripremu" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Current Purchase Requisition" +msgstr "Trenutni zahtjev nabavke" + +#. module: purchase_requisition +#: model:res.groups,name:purchase_requisition.group_purchase_requisition_user +msgid "User" +msgstr "Korisnik" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Order Reference" +msgstr "Referenca narudžbe" + +#. module: purchase_requisition +#: field:purchase.requisition,message_is_follower:0 +msgid "Is a Follower" +msgstr "Je pratilac" + +#. module: purchase_requisition +#: field:purchase.requisition.line,product_qty:0 +msgid "Quantity" +msgstr "Količina" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Unassigned Requisition" +msgstr "Nedodjeljeni zahtjevi" + +#. module: purchase_requisition +#: model:ir.actions.act_window,name:purchase_requisition.action_purchase_requisition +#: model:ir.ui.menu,name:purchase_requisition.menu_purchase_requisition_pro_mgt +msgid "Purchase Requisitions" +msgstr "Nabavni zahtjevi" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Quotation Detail" +msgstr "Detalji predračuna" + +#. module: purchase_requisition +#: code:addons/purchase_requisition/purchase_requisition.py:134 +#, python-format +msgid "" +"You have already one %s purchase order for this partner, you must cancel " +"this purchase order to create a new quotation." +msgstr "" +"Već imate jedan %s nabavnu narudžbu za tog partnera. Morate poništiti ovau " +"nabavnu narudžbu kako bi kreirali novi predračun." + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "End Date" +msgstr "Krajnji datum" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +#: field:purchase.requisition,name:0 +msgid "Requisition Reference" +msgstr "Referenca zahtjeva" + +#. module: purchase_requisition +#: field:purchase.requisition,line_ids:0 +msgid "Products to Purchase" +msgstr "Proizvodi za nabavku" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +#: selection:purchase.requisition,state:0 +msgid "Sent to Suppliers" +msgstr "Pošalji dobavljačima" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Search Purchase Requisition" +msgstr "Pretraži zahtjeve nabavke" + +#. module: purchase_requisition +#: code:addons/purchase_requisition/wizard/purchase_requisition_partner.py:41 +#, python-format +msgid "No Product in Tender." +msgstr "Nema proizvoda na tenderu." + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Date Ordered" +msgstr "Datum naruđbe" + +#. module: purchase_requisition +#: field:purchase.requisition,message_ids:0 +msgid "Messages" +msgstr "Poruke" + +#. module: purchase_requisition +#: help:purchase.requisition,exclusive:0 +msgid "" +"Purchase Requisition (exclusive): On the confirmation of a purchase order, " +"it cancels the remaining purchase order.\n" +"Purchase Requisition(Multiple): It allows to have multiple purchase " +"orders.On confirmation of a purchase order it does not cancel the remaining " +"orders" +msgstr "" +"Zahtjev nabavke (ekskluzivni): Prilikom potvrde nabavne narudžbe , " +"poništava preostale nabavne narudžbe.\n" +"Zahtjev nabavke(višestruki): Omogućuje više nabavnih narudžbi. Prilikom " +"potvrde nabavne narudžbe ne poništava preostale narudžbe." + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Cancel Purchase Order" +msgstr "Otkaži nabavnu narudžbu" + +#. module: purchase_requisition +#: model:ir.model,name:purchase_requisition.model_purchase_order +#: view:purchase.requisition:0 +msgid "Purchase Order" +msgstr "Nabavna narudžba" + +#. module: purchase_requisition +#: field:purchase.requisition,origin:0 +msgid "Source Document" +msgstr "Izvorni dokument" + +#. module: purchase_requisition +#: code:addons/purchase_requisition/wizard/purchase_requisition_partner.py:41 +#, python-format +msgid "Error!" +msgstr "Greška!" + +#. module: purchase_requisition +#: field:purchase.requisition,exclusive:0 +msgid "Requisition Type" +msgstr "Vrsta zahtjeva" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "New Purchase Requisition" +msgstr "Novi zahtjev nabavke" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Products" +msgstr "Proizvodi" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Order Date" +msgstr "Datum narudžbe" + +#. module: purchase_requisition +#: selection:purchase.requisition,state:0 +msgid "Cancelled" +msgstr "Otkazano" + +#. module: purchase_requisition +#: model:ir.model,name:purchase_requisition.model_purchase_requisition_partner +msgid "Purchase Requisition Partner" +msgstr "Parner zahtjeva nabavke" + +#. module: purchase_requisition +#: help:purchase.requisition,message_unread:0 +msgid "If checked new messages require your attention." +msgstr "Ako je označeno, nove poruke će zahtjevati vašu pažnju." + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Purchase for Requisitions" +msgstr "Nabava za zahtjeve" + +#. module: purchase_requisition +#: model:ir.actions.act_window,name:purchase_requisition.act_res_partner_2_purchase_order +msgid "Purchase orders" +msgstr "Nabavne narudžbe" + +#. module: purchase_requisition +#: field:purchase.requisition,date_end:0 +msgid "Requisition Deadline" +msgstr "Konačan rok zahtjeva" + +#. module: purchase_requisition +#: field:purchase.requisition,message_summary:0 +msgid "Summary" +msgstr "Rezime" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Reference" +msgstr "Referenca" + +#. module: purchase_requisition +#: model:ir.model,name:purchase_requisition.model_procurement_order +msgid "Procurement" +msgstr "Naručivanje" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +#: view:purchase.requisition:0 +msgid "Source" +msgstr "Izvor" + +#. module: purchase_requisition +#: field:purchase.requisition,warehouse_id:0 +msgid "Warehouse" +msgstr "Skladište" + +#. module: purchase_requisition +#: field:procurement.order,requisition_id:0 +msgid "Latest Requisition" +msgstr "Zadnji zahtjev" + +#. module: purchase_requisition +#: model:res.groups,name:purchase_requisition.group_purchase_requisition_manager +msgid "Manager" +msgstr "Menadžer" + +#. module: purchase_requisition +#: selection:purchase.requisition,exclusive:0 +msgid "Purchase Requisition (exclusive)" +msgstr "Zahtjev nabavke (ekskluzivan)" + +#. module: purchase_requisition +#: help:purchase.requisition,message_summary:0 +msgid "" +"Holds the Chatter summary (number of messages, ...). This summary is " +"directly in html format in order to be inserted in kanban views." +msgstr "" +"Sadrži sažetak konverzacije (broj poruka,..). Ovaj sažetak je u html formatu " +"da bi mogao biti ubačen u kanban pogled." + +#. module: purchase_requisition +#: report:purchase.requisition:0 +msgid "Product UoM" +msgstr "Proizvod UoM" + +#. module: purchase_requisition +#: code:addons/purchase_requisition/purchase_requisition.py:134 +#, python-format +msgid "Warning!" +msgstr "Upozorenje!" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Confirm Purchase Order" +msgstr "Potvrdi nabavni nalog" + +#. module: purchase_requisition +#: view:purchase.requisition.partner:0 +msgid "Cancel" +msgstr "Otkaži" + +#. module: purchase_requisition +#: report:purchase.requisition:0 +#: field:purchase.requisition,date_start:0 +msgid "Requisition Date" +msgstr "Datum zahtjeva" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Start Date" +msgstr "Datum početka" + +#. module: purchase_requisition +#: view:purchase.requisition:0 +msgid "Unassigned" +msgstr "Nedodeljen" + +#. module: purchase_requisition +#: view:purchase.order:0 +msgid "Requisition" +msgstr "Zahtjev" + +#. module: purchase_requisition +#: help:product.product,purchase_requisition:0 +msgid "" +"Check this box to generates purchase requisition instead of generating " +"requests for quotation from procurement." +msgstr "" +"Označite ovu kućicu da generišete zahtjeve nabavke umjesto generisanja " +"zahtjeva za predračunom iz naručivanja." + +#. module: purchase_requisition +#: field:purchase.requisition,purchase_ids:0 +msgid "Purchase Orders" +msgstr "Nabavne narudžbe" diff --git a/addons/resource/i18n/bs.po b/addons/resource/i18n/bs.po new file mode 100644 index 00000000000..596d4c7d60d --- /dev/null +++ b/addons/resource/i18n/bs.po @@ -0,0 +1,364 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 13:41+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: resource +#: help:resource.calendar.leaves,resource_id:0 +msgid "" +"If empty, this is a generic holiday for the company. If a resource is set, " +"the holiday/leave is only for this resource" +msgstr "" +"Ako je prazno, ovo je generički praznik za kompaniju. Ako je resurs " +"postavljen, praznik/odsustvo je samo za taj resurs." + +#. module: resource +#: selection:resource.resource,resource_type:0 +msgid "Material" +msgstr "Materijal" + +#. module: resource +#: field:resource.resource,resource_type:0 +msgid "Resource Type" +msgstr "Tip resursa" + +#. module: resource +#: model:ir.model,name:resource.model_resource_calendar_leaves +#: view:resource.calendar.leaves:0 +msgid "Leave Detail" +msgstr "Detalji o odsustvu" + +#. module: resource +#: model:ir.actions.act_window,name:resource.resource_calendar_resources_leaves +msgid "Resources Leaves" +msgstr "Odsustva resursa" + +#. module: resource +#: field:resource.calendar.attendance,dayofweek:0 +msgid "Day of Week" +msgstr "Dan u sedmici" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Thursday" +msgstr "Četvrtak" + +#. module: resource +#: view:resource.calendar.leaves:0 +#: view:resource.resource:0 +msgid "Group By..." +msgstr "Grupiši po..." + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Sunday" +msgstr "Nedjelja" + +#. module: resource +#: field:resource.resource,time_efficiency:0 +msgid "Efficiency Factor" +msgstr "Faktor efikasnosti" + +#. module: resource +#: view:resource.resource:0 +msgid "Search Resource" +msgstr "Pretraži resurse" + +#. module: resource +#: view:resource.resource:0 +msgid "Type" +msgstr "Tip" + +#. module: resource +#: model:ir.actions.act_window,name:resource.action_resource_resource_tree +#: view:resource.resource:0 +msgid "Resources" +msgstr "Resursi" + +#. module: resource +#: code:addons/resource/resource.py:455 +#, python-format +msgid "Make sure the Working time has been configured with proper week days!" +msgstr "" +"Obezbjedite da je radno vrijeme pravilno konfigurisano sa danima sedmice!" + +#. module: resource +#: code:addons/resource/resource.py:373 +#, python-format +msgid "%s (copy)" +msgstr "%s (kopija)" + +#. module: resource +#: view:resource.calendar:0 +msgid "Search Working Time" +msgstr "Pretraži radno vrijeme" + +#. module: resource +#: constraint:resource.calendar.leaves:0 +msgid "Error! leave start-date must be lower then leave end-date." +msgstr "" +"Greška! Datum početka odsustva mora biti manji od završnog datuma odsustva" + +#. module: resource +#: model:ir.model,name:resource.model_resource_calendar +msgid "Resource Calendar" +msgstr "Kalendar resursa" + +#. module: resource +#: field:resource.calendar,company_id:0 +#: view:resource.calendar.leaves:0 +#: field:resource.calendar.leaves,company_id:0 +#: view:resource.resource:0 +#: field:resource.resource,company_id:0 +msgid "Company" +msgstr "Kompanija" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Friday" +msgstr "Petak" + +#. module: resource +#: view:resource.calendar.attendance:0 +msgid "Hours" +msgstr "Sati" + +#. module: resource +#: view:resource.calendar.leaves:0 +msgid "Reason" +msgstr "Razlog" + +#. module: resource +#: view:resource.resource:0 +#: field:resource.resource,user_id:0 +msgid "User" +msgstr "Korisnik" + +#. module: resource +#: view:resource.calendar.leaves:0 +msgid "Date" +msgstr "Datum" + +#. module: resource +#: view:resource.calendar.leaves:0 +msgid "Search Working Period Leaves" +msgstr "Pretraži odsustva u periodima rada" + +#. module: resource +#: field:resource.calendar.attendance,date_from:0 +msgid "Starting Date" +msgstr "Datum početka" + +#. module: resource +#: field:resource.calendar,manager:0 +msgid "Workgroup Manager" +msgstr "Voditelj radne grupe" + +#. module: resource +#: field:resource.calendar.leaves,date_to:0 +msgid "End Date" +msgstr "Datum Završetka" + +#. module: resource +#: model:ir.actions.act_window,name:resource.resource_calendar_closing_days +msgid "Closing Days" +msgstr "Dani zatvaranja" + +#. module: resource +#: model:ir.ui.menu,name:resource.menu_resource_config +#: view:resource.calendar.leaves:0 +#: field:resource.calendar.leaves,resource_id:0 +#: view:resource.resource:0 +msgid "Resource" +msgstr "Resurs" + +#. module: resource +#: field:resource.calendar,name:0 +#: field:resource.calendar.attendance,name:0 +#: field:resource.calendar.leaves,name:0 +#: field:resource.resource,name:0 +msgid "Name" +msgstr "Naziv" + +#. module: resource +#: model:ir.actions.act_window,name:resource.action_resource_calendar_form +#: view:resource.calendar:0 +#: field:resource.calendar,attendance_ids:0 +#: view:resource.calendar.attendance:0 +#: field:resource.calendar.leaves,calendar_id:0 +#: field:resource.resource,calendar_id:0 +msgid "Working Time" +msgstr "Radno Vrijeme" + +#. module: resource +#: help:resource.calendar.attendance,hour_from:0 +msgid "Start and End time of working." +msgstr "Početno i krajnje vrijeme rada." + +#. module: resource +#: view:resource.calendar.leaves:0 +#: view:resource.resource:0 +msgid "Working Period" +msgstr "Period rada" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Wednesday" +msgstr "Srijeda" + +#. module: resource +#: model:ir.model,name:resource.model_resource_resource +msgid "Resource Detail" +msgstr "Detalji resursa" + +#. module: resource +#: field:resource.resource,active:0 +msgid "Active" +msgstr "Aktivan" + +#. module: resource +#: help:resource.resource,active:0 +msgid "" +"If the active field is set to False, it will allow you to hide the resource " +"record without removing it." +msgstr "" +"Ako je ovo polje postavljeno na neaktivno, moežte sakriti resurs bez da ga " +"uklonite." + +#. module: resource +#: field:resource.calendar.attendance,calendar_id:0 +msgid "Resource's Calendar" +msgstr "Kalendar resursa" + +#. module: resource +#: field:resource.calendar.attendance,hour_from:0 +msgid "Work from" +msgstr "Radi od" + +#. module: resource +#: model:ir.actions.act_window,help:resource.action_resource_calendar_form +msgid "" +"Define working hours and time table that could be scheduled to your project " +"members" +msgstr "" + +#. module: resource +#: help:resource.resource,user_id:0 +msgid "Related user name for the resource to manage its access." +msgstr "Povezano korisničko ime za resurs da upravlja njegovim pristupom." + +#. module: resource +#: help:resource.resource,calendar_id:0 +msgid "Define the schedule of resource" +msgstr "Definišite zakazivanje resursa" + +#. module: resource +#: view:resource.calendar.leaves:0 +msgid "Starting Date of Leave" +msgstr "Datum početka odsustva" + +#. module: resource +#: field:resource.resource,code:0 +msgid "Code" +msgstr "Šifra" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Monday" +msgstr "Ponedjeljak" + +#. module: resource +#: field:resource.calendar.attendance,hour_to:0 +msgid "Work to" +msgstr "Radi do" + +#. module: resource +#: model:ir.model,name:resource.model_resource_calendar_attendance +msgid "Work Detail" +msgstr "Detalji rada" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Tuesday" +msgstr "Utorak" + +#. module: resource +#: help:resource.resource,time_efficiency:0 +msgid "" +"This field depict the efficiency of the resource to complete tasks. e.g " +"resource put alone on a phase of 5 days with 5 tasks assigned to him, will " +"show a load of 100% for this phase by default, but if we put a efficiency of " +"200%, then his load will only be 50%." +msgstr "" +"Ovo polje razlikuje efikasnost resursa za završetak zadatka. npr.: resurs " +"postavljen sam na fazu od 5 dana sa 5 zadataka dodjelenih njemu, će pokazati " +"opterećenje od 100% za ovu fazu zadano, ali ako postavimo efikasnost od " +"200%, tada će ovo opterećenje iznositi samo 50%." + +#. module: resource +#: model:ir.actions.act_window,name:resource.action_resource_calendar_leave_tree +#: model:ir.ui.menu,name:resource.menu_view_resource_calendar_leaves_search +msgid "Resource Leaves" +msgstr "Odsustva resursa" + +#. module: resource +#: model:ir.actions.act_window,help:resource.action_resource_resource_tree +msgid "" +"Resources allow you to create and manage resources that should be involved " +"in a specific project phase. You can also set their efficiency level and " +"workload based on their weekly working hours." +msgstr "" +"Resursi Vam omogućuju kreiranje i upravljanje resursima koji trebaju biti " +"uključeni u neku fazu projekta. Također možete postaviti njihov nivo " +"efikasnosti i opterećenje na osnovi njihovih sedmičnih radnih sati." + +#. module: resource +#: view:resource.resource:0 +msgid "Inactive" +msgstr "Neaktivan" + +#. module: resource +#: code:addons/resource/faces/resource.py:340 +#, python-format +msgid "(vacation)" +msgstr "(odmor)" + +#. module: resource +#: code:addons/resource/resource.py:455 +#, python-format +msgid "Configuration Error!" +msgstr "Greška u konfiguraciji!" + +#. module: resource +#: selection:resource.resource,resource_type:0 +msgid "Human" +msgstr "Čovjek" + +#. module: resource +#: view:resource.calendar.leaves:0 +msgid "Duration" +msgstr "Trajanje" + +#. module: resource +#: field:resource.calendar.leaves,date_from:0 +msgid "Start Date" +msgstr "Datum početka" + +#. module: resource +#: selection:resource.calendar.attendance,dayofweek:0 +msgid "Saturday" +msgstr "Subota" diff --git a/addons/sale_margin/i18n/bs.po b/addons/sale_margin/i18n/bs.po new file mode 100644 index 00000000000..a8815bd00ff --- /dev/null +++ b/addons/sale_margin/i18n/bs.po @@ -0,0 +1,48 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 13:07+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: sale_margin +#: field:sale.order.line,purchase_price:0 +msgid "Cost Price" +msgstr "Cijena koštanja" + +#. module: sale_margin +#: model:ir.model,name:sale_margin.model_sale_order +msgid "Sales Order" +msgstr "Prodajna narudžba" + +#. module: sale_margin +#: field:sale.order,margin:0 +#: field:sale.order.line,margin:0 +msgid "Margin" +msgstr "Marža" + +#. module: sale_margin +#: model:ir.model,name:sale_margin.model_sale_order_line +msgid "Sales Order Line" +msgstr "Stavka prodajne narudžbe" + +#. module: sale_margin +#: help:sale.order,margin:0 +msgid "" +"It gives profitability by calculating the difference between the Unit Price " +"and the cost price." +msgstr "" +"Daje Vam profitabilnost računajući razliku između jedinične cijene i cijene " +"koštanja." diff --git a/addons/sale_mrp/i18n/bs.po b/addons/sale_mrp/i18n/bs.po new file mode 100644 index 00000000000..9012ade8ceb --- /dev/null +++ b/addons/sale_mrp/i18n/bs.po @@ -0,0 +1,43 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 13:09+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: sale_mrp +#: model:ir.model,name:sale_mrp.model_mrp_production +msgid "Manufacturing Order" +msgstr "Radni nalog proizvodnje" + +#. module: sale_mrp +#: help:mrp.production,sale_name:0 +msgid "Indicate the name of sales order." +msgstr "Indicira naziv prodajne narudžbe." + +#. module: sale_mrp +#: help:mrp.production,sale_ref:0 +msgid "Indicate the Customer Reference from sales order." +msgstr "Indicira referencu kupca iz prodajne narudžbe." + +#. module: sale_mrp +#: field:mrp.production,sale_ref:0 +msgid "Sale Reference" +msgstr "Prodajna referenca" + +#. module: sale_mrp +#: field:mrp.production,sale_name:0 +msgid "Sale Name" +msgstr "Naziv prodaje" diff --git a/addons/sale_order_dates/i18n/bs.po b/addons/sale_order_dates/i18n/bs.po new file mode 100644 index 00000000000..03dbb97c7f7 --- /dev/null +++ b/addons/sale_order_dates/i18n/bs.po @@ -0,0 +1,58 @@ +# Bosnian translation for openobject-addons +# Copyright (c) 2013 Rosetta Contributors and Canonical Ltd 2013 +# This file is distributed under the same license as the openobject-addons package. +# FIRST AUTHOR , 2013. +# +msgid "" +msgstr "" +"Project-Id-Version: openobject-addons\n" +"Report-Msgid-Bugs-To: FULL NAME \n" +"POT-Creation-Date: 2012-12-21 17:06+0000\n" +"PO-Revision-Date: 2013-10-27 13:13+0000\n" +"Last-Translator: FULL NAME \n" +"Language-Team: Bosnian \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Launchpad-Export-Date: 2013-10-28 05:23+0000\n" +"X-Generator: Launchpad (build 16810)\n" + +#. module: sale_order_dates +#: view:sale.order:0 +msgid "Dates" +msgstr "Datumi" + +#. module: sale_order_dates +#: field:sale.order,commitment_date:0 +msgid "Commitment Date" +msgstr "Obavezani datum" + +#. module: sale_order_dates +#: field:sale.order,effective_date:0 +msgid "Effective Date" +msgstr "Efektivni datum" + +#. module: sale_order_dates +#: help:sale.order,effective_date:0 +msgid "Date on which picking is created." +msgstr "Datum kada je prikupljanje proizvoda kreirano." + +#. module: sale_order_dates +#: help:sale.order,requested_date:0 +msgid "Date requested by the customer for the sale." +msgstr "Zahtjevani datum prodaje od kupca" + +#. module: sale_order_dates +#: field:sale.order,requested_date:0 +msgid "Requested Date" +msgstr "Zahtjevani datum" + +#. module: sale_order_dates +#: model:ir.model,name:sale_order_dates.model_sale_order +msgid "Sales Order" +msgstr "Prodajna narudžba" + +#. module: sale_order_dates +#: help:sale.order,commitment_date:0 +msgid "Committed date for delivery." +msgstr "Potvrđen datum dostave."