# -*- coding: utf-8 -*- import glob, os import pprint from xml.etree import ElementTree from cStringIO import StringIO import simplejson import openerpweb import openerpweb.ast import openerpweb.nonliterals import cherrypy # Should move to openerpweb.Xml2Json class Xml2Json: # xml2json-direct # Simple and straightforward XML-to-JSON converter in Python # New BSD Licensed # # URL: http://code.google.com/p/xml2json-direct/ @staticmethod def convert_to_json(s): return simplejson.dumps( Xml2Json.convert_to_structure(s), sort_keys=True, indent=4) @staticmethod def convert_to_structure(s): root = ElementTree.fromstring(s) return Xml2Json.convert_element(root) @staticmethod def convert_element(el, skip_whitespaces=True): res = {} if el.tag[0] == "{": ns, name = el.tag.rsplit("}", 1) res["tag"] = name res["namespace"] = ns[1:] else: res["tag"] = el.tag res["attrs"] = {} for k, v in el.items(): res["attrs"][k] = v kids = [] if el.text and (not skip_whitespaces or el.text.strip() != ''): kids.append(el.text) for kid in el: kids.append(Xml2Json.convert_element(kid)) if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''): kids.append(kid.tail) res["children"] = kids return res #---------------------------------------------------------- # OpenERP Web base Controllers #---------------------------------------------------------- class Session(openerpweb.Controller): _cp_path = "/base/session" def manifest_glob(self, addons, key): files = [] for addon in addons: globlist = openerpweb.addons_manifest.get(addon, {}).get(key, []) files.extend([ resource_path[len(openerpweb.path_addons):] for pattern in globlist for resource_path in glob.glob(os.path.join( openerpweb.path_addons, addon, pattern)) ]) return files def concat_files(self, file_list): """ Concatenate file content return (concat,timestamp) concat: concatenation of file content timestamp: max(os.path.getmtime of file_list) """ root = openerpweb.path_root files_content = [] files_timestamp = 0 for i in file_list: fname = os.path.join(root, i) ftime = os.path.getmtime(fname) if ftime > files_timestamp: files_timestamp = ftime files_content = open(fname).read() files_concat = "".join(files_content) return files_concat @openerpweb.jsonrequest def login(self, req, db, login, password): req.session.login(db, login, password) return { "session_id": req.session_id, "uid": req.session._uid, } @openerpweb.jsonrequest def modules(self, req): return {"modules": [name for name, manifest in openerpweb.addons_manifest.iteritems() if manifest.get('active', True)]} @openerpweb.jsonrequest def csslist(self, req, mods='base'): return {'files': self.manifest_glob(mods.split(','), 'css')} @openerpweb.jsonrequest def jslist(self, req, mods='base'): return {'files': self.manifest_glob(mods.split(','), 'js')} def css(self, req, mods='base,base_hello'): files = self.manifest_glob(mods.split(','), 'css') concat = self.concat_files(files)[0] # TODO request set the Date of last modif and Etag return concat css.exposed = True def js(self, req, mods='base,base_hello'): files = self.manifest_glob(mods.split(','), 'js') concat = self.concat_files(files)[0] # TODO request set the Date of last modif and Etag return concat js.exposed = True @openerpweb.jsonrequest def eval_domain_and_context(self, req, contexts, domains, group_by_seq=None): """ Evaluates sequences of domains and contexts, composing them into a single context, domain or group_by sequence. :param list contexts: list of contexts to merge together. Contexts are evaluated in sequence, all previous contexts are part of their own evaluation context (starting at the session context). :param list domains: list of domains to merge together. Domains are evaluated in sequence and appended to one another (implicit AND), their evaluation domain is the result of merging all contexts. :param list group_by_seq: list of domains (which may be in a different order than the ``contexts`` parameter), evaluated in sequence, their ``'group_by'`` key is extracted if they have one. :returns: a 3-dict of: context (``dict``) the global context created by merging all of ``contexts`` domain (``list``) the concatenation of all domains group_by (``list``) a list of fields to group by, potentially empty (in which case no group by should be performed) """ context = req.session.eval_contexts(contexts) domain = req.session.eval_domains(domains, context) group_by_sequence = [] for candidate in (group_by_seq or []): ctx = req.session.eval_context(candidate, context) group_by = ctx.get('group_by') if not group_by: continue elif isinstance(group_by, basestring): group_by_sequence.append(group_by) else: group_by_sequence.extend(group_by) return { 'context': context, 'domain': domain, 'group_by': group_by_sequence } @openerpweb.jsonrequest def save_session_action(self, req, the_action): """ This method store an action object in the session object and returns an integer identifying that action. The method get_session_action() can be used to get back the action. :param the_action: The action to save in the session. :type the_action: anything :return: A key identifying the saved action. :rtype: integer """ saved_actions = cherrypy.session.get('saved_actions') if not saved_actions: saved_actions = {"next":0, "actions":{}} cherrypy.session['saved_actions'] = saved_actions # we don't allow more than 10 stored actions if len(saved_actions["actions"]) >= 10: del saved_actions["actions"][min(saved_actions["actions"].keys())] key = saved_actions["next"] saved_actions["actions"][key] = the_action saved_actions["next"] = key + 1 return key @openerpweb.jsonrequest def get_session_action(self, req, key): """ Gets back a previously saved action. This method can return None if the action was saved since too much time (this case should be handled in a smart way). :param key: The key given by save_session_action() :type key: integer :return: The saved action or None. :rtype: anything """ saved_actions = cherrypy.session.get('saved_actions') if not saved_actions: return None return saved_actions["actions"].get(key) def load_actions_from_ir_values(req, key, key2, models, meta, context): Values = req.session.model('ir.values') actions = Values.get(key, key2, models, meta, context) for _, _, action in actions: clean_action(action, req.session) return actions def clean_action(action, session): # values come from the server, we can just eval them if isinstance(action['context'], basestring): action['context'] = eval( action['context'], session.evaluation_context()) or {} if isinstance(action['domain'], basestring): action['domain'] = eval( action['domain'], session.evaluation_context( action['context'])) or [] fix_view_modes(action) def fix_view_modes(action): """ For historical reasons, OpenERP has weird dealings in relation to view_mode and the view_type attribute (on window actions): * one of the view modes is ``tree``, which stands for both list views and tree views * the choice is made by checking ``view_type``, which is either ``form`` for a list view or ``tree`` for an actual tree view This methods simply folds the view_type into view_mode by adding a new view mode ``list`` which is the result of the ``tree`` view_mode in conjunction with the ``form`` view_type. TODO: this should go into the doc, some kind of "peculiarities" section :param dict action: an action descriptor :returns: nothing, the action is modified in place """ if action.pop('view_type') != 'form': return action['view_mode'] = ','.join( mode if mode != 'tree' else 'list' for mode in action['view_mode'].split(',')) action['views'] = [ [id, mode if mode != 'tree' else 'list'] for id, mode in action['views'] ] class Menu(openerpweb.Controller): _cp_path = "/base/menu" @openerpweb.jsonrequest def load(self, req): return {'data': self.do_load(req)} def do_load(self, req): """ Loads all menu items (all applications and their sub-menus). :param req: A request object, with an OpenERP session attribute :type req: < session -> OpenERPSession > :return: the menu root :rtype: dict('children': menu_nodes) """ Menus = req.session.model('ir.ui.menu') # menus are loaded fully unlike a regular tree view, cause there are # less than 512 items menu_ids = Menus.search([]) menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id']) menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']} menu_items.append(menu_root) # make a tree using parent_id menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items) for menu_item in menu_items: if menu_item['parent_id']: parent = menu_item['parent_id'][0] else: parent = False if parent in menu_items_map: menu_items_map[parent].setdefault( 'children', []).append(menu_item) # sort by sequence a tree using parent_id for menu_item in menu_items: menu_item.setdefault('children', []).sort( key=lambda x:x["sequence"]) return menu_root @openerpweb.jsonrequest def action(self, req, menu_id): actions = load_actions_from_ir_values(req,'action', 'tree_but_open', [('ir.ui.menu', menu_id)], False, {}) return {"action": actions} class DataSet(openerpweb.Controller): _cp_path = "/base/dataset" @openerpweb.jsonrequest def fields(self, req, model): return {'fields': req.session.model(model).fields_get()} @openerpweb.jsonrequest def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None): return self.do_search_read(request, model, fields, offset, limit, domain, context, sort) def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None): """ Performs a search() followed by a read() (if needed) using the provided search criteria :param request: a JSON-RPC request object :type request: openerpweb.JsonRequest :param str model: the name of the model to search on :param fields: a list of the fields to return in the result records :type fields: [str] :param int offset: from which index should the results start being returned :param int limit: the maximum number of records to return :param list domain: the search domain for the query :param list sort: sorting directives :returns: a list of result records :rtype: list """ Model = request.session.model(model) ids = Model.search(domain or [], offset or 0, limit or False, sort or False, request.context) if fields and fields == ['id']: # shortcut read if we only want the ids return map(lambda id: {'id': id}, ids) return Model.read(ids, fields or False, request.context) @openerpweb.jsonrequest def get(self, request, model, ids, fields=False): return self.do_get(request, model, ids, fields) def do_get(self, request, model, ids, fields=False): """ Fetches and returns the records of the model ``model`` whose ids are in ``ids``. The results are in the same order as the inputs, but elements may be missing (if there is no record left for the id) :param request: the JSON-RPC2 request object :type request: openerpweb.JsonRequest :param model: the model to read from :type model: str :param ids: a list of identifiers :type ids: list :param fields: a list of fields to fetch, ``False`` or empty to fetch all fields in the model :type fields: list | False :returns: a list of records, in the same order as the list of ids :rtype: list """ Model = request.session.model(model) records = Model.read(ids, fields) record_map = dict((record['id'], record) for record in records) return [record_map[id] for id in ids if record_map.get(id)] @openerpweb.jsonrequest def load(self, req, model, id, fields): m = req.session.model(model) value = {} r = m.read([id]) if r: value = r[0] return {'value': value} @openerpweb.jsonrequest def create(self, req, model, data, context={}): m = req.session.model(model) r = m.create(data, context) return {'result': r} @openerpweb.jsonrequest def save(self, req, model, id, data, context={}): m = req.session.model(model) r = m.write([id], data, context) return {'result': r} @openerpweb.jsonrequest def call(self, req, model, method, ids, args): m = req.session.model(model) r = getattr(m, method)(ids, *args) return {'result': r} @openerpweb.jsonrequest def exec_workflow(self, req, model, id, signal): r = req.session.exec_workflow(model, id, signal) return {'result': r} @openerpweb.jsonrequest def default_get(self, req, model, fields, context={}): m = req.session.model(model) r = m.default_get(fields, context) return {'result': r} class View(openerpweb.Controller): def fields_view_get(self, request, model, view_id, view_type, transform=True, toolbar=False, submenu=False): Model = request.session.model(model) fvg = Model.fields_view_get(view_id, view_type, request.context, toolbar, submenu) if transform: evaluation_context = request.session.evaluation_context( request.context or {}) xml = self.transform_view( fvg['arch'], request.session, evaluation_context) else: xml = ElementTree.fromstring(fvg['arch']) fvg['arch'] = Xml2Json.convert_element(xml) return fvg def normalize_attrs(self, elem, context): """ Normalize @attrs, @invisible, @required, @readonly and @states, so the client only has to deal with @attrs. See `the discoveries pad `_ for the rationale. :param elem: the current view node (Python object) :type elem: xml.etree.ElementTree.Element :param dict context: evaluation context """ # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}')) if 'states' in elem.attrib: attrs.setdefault('invisible', [])\ .append(('state', 'not in', elem.attrib.pop('states').split(','))) if attrs: elem.set('attrs', simplejson.dumps(attrs)) for a in ['invisible', 'readonly', 'required']: if a in elem.attrib: # In the XML we trust avalue = bool(eval(elem.get(a, 'False'), {'context': context or {}})) if not avalue: del elem.attrib[a] else: elem.attrib[a] = '1' if a == 'invisible' and 'attrs' in elem.attrib: del elem.attrib['attrs'] def transform_view(self, view_string, session, context=None): # transform nodes on the fly via iterparse, instead of # doing it statically on the parsing result parser = ElementTree.iterparse(StringIO(view_string), events=("start",)) root = None for event, elem in parser: if event == "start": if root is None: root = elem self.normalize_attrs(elem, context) self.parse_domains_and_contexts(elem, session) return root def parse_domain(self, elem, attr_name, session): """ Parses an attribute of the provided name as a domain, transforms it to either a literal domain or a :class:`openerpweb.nonliterals.Domain` :param elem: the node being parsed :type param: xml.etree.ElementTree.Element :param str attr_name: the name of the attribute which should be parsed :param session: Current OpenERP session :type session: openerpweb.openerpweb.OpenERPSession """ domain = elem.get(attr_name, '').strip() if domain: try: elem.set( attr_name, openerpweb.ast.literal_eval( domain)) except ValueError: # not a literal elem.set(attr_name, openerpweb.nonliterals.Domain(session, domain)) def parse_domains_and_contexts(self, elem, session): """ Converts domains and contexts from the view into Python objects, either literals if they can be parsed by literal_eval or a special placeholder object if the domain or context refers to free variables. :param elem: the current node being parsed :type param: xml.etree.ElementTree.Element :param session: OpenERP session object, used to store and retrieve non-literal objects :type session: openerpweb.openerpweb.OpenERPSession """ self.parse_domain(elem, 'domain', session) self.parse_domain(elem, 'filter_domain', session) context_string = elem.get('context', '').strip() if context_string: try: elem.set('context', openerpweb.ast.literal_eval(context_string)) except ValueError: elem.set('context', openerpweb.nonliterals.Context( session, context_string)) class FormView(View): _cp_path = "/base/formview" @openerpweb.jsonrequest def load(self, req, model, view_id, toolbar=False): fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar) return {'fields_view': fields_view} class ListView(View): _cp_path = "/base/listview" @openerpweb.jsonrequest def load(self, req, model, view_id, toolbar=False): fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar) return {'fields_view': fields_view} def fields_view_get(self, request, model, view_id, view_type="tree", transform=True, toolbar=False, submenu=False): """ Sets @editable on the view's arch if it isn't already set and ``set_editable`` is present in the request context """ view = super(ListView, self).fields_view_get( request, model, view_id, view_type, transform, toolbar, submenu) view_attributes = view['arch']['attrs'] if request.context.get('set_editable')\ and 'editable' not in view_attributes: view_attributes['editable'] = 'bottom' return view @openerpweb.jsonrequest def fill(self, request, model, id, domain, offset=0, limit=False): return self.do_fill(request, model, id, domain, offset, limit) def do_fill(self, request, model, id, domain, offset=0, limit=False): """ Returns all information needed to fill a table: * view with processed ``editable`` flag * fields (columns) with processed ``invisible`` flag * rows with processed ``attrs`` and ``colors`` .. note:: context is passed through ``request`` parameter :param request: OpenERP request :type request: openerpweb.openerpweb.JsonRequest :type str model: OpenERP model for this list view :type int id: view_id, or False if none provided :param list domain: the search domain to search for :param int offset: search offset, for pagination :param int limit: search limit, for pagination :returns: hell if I have any idea yet """ view = self.fields_view_get(request, model, id, toolbar=True) rows = DataSet().do_search_read(request, model, offset=offset, limit=limit, domain=domain) eval_context = request.session.evaluation_context( request.context) return { 'view': view, 'records': [ {'data': dict((key, {'value': value}) for key, value in row.iteritems()), 'color': self.process_colors(view, row, eval_context)} for row in rows ] } def process_colors(self, view, row, context): colors = view['arch']['attrs'].get('colors') if not colors: return None color = [ pair.split(':')[0] for pair in colors.split(';') if eval(pair.split(':')[1], dict(context, **row)) ] if not color: return None elif len(color) == 1: return color[0] return 'maroon' class SearchView(View): _cp_path = "/base/searchview" @openerpweb.jsonrequest def load(self, req, model, view_id): fields_view = self.fields_view_get(req, model, view_id, 'search') return {'fields_view': fields_view} class Action(openerpweb.Controller): _cp_path = "/base/action" @openerpweb.jsonrequest def load(self, req, action_id, context={}): Actions = req.session.model('ir.actions.actions') value = False action_type = Actions.read([action_id], ['type'], context) if action_type: action = req.session.model(action_type[0]['type']).read([action_id], False, context) if action: value = action[0] return {'value': value}