1 # -*- coding: utf-8 -*-
5 from xml.etree import ElementTree
6 from cStringIO import StringIO
12 import openerpweb.nonliterals
16 # Should move to openerpweb.Xml2Json
19 # Simple and straightforward XML-to-JSON converter in Python
22 # URL: http://code.google.com/p/xml2json-direct/
24 def convert_to_json(s):
25 return simplejson.dumps(
26 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
29 def convert_to_structure(s):
30 root = ElementTree.fromstring(s)
31 return Xml2Json.convert_element(root)
34 def convert_element(el, skip_whitespaces=True):
37 ns, name = el.tag.rsplit("}", 1)
39 res["namespace"] = ns[1:]
43 for k, v in el.items():
46 if el.text and (not skip_whitespaces or el.text.strip() != ''):
49 kids.append(Xml2Json.convert_element(kid))
50 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
52 res["children"] = kids
55 #----------------------------------------------------------
56 # OpenERP Web base Controllers
57 #----------------------------------------------------------
59 class Session(openerpweb.Controller):
60 _cp_path = "/base/session"
62 def manifest_glob(self, addons, key):
65 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
68 resource_path[len(openerpweb.path_addons):]
69 for pattern in globlist
70 for resource_path in glob.glob(os.path.join(
71 openerpweb.path_addons, addon, pattern))
75 def concat_files(self, file_list):
76 """ Concatenate file content
77 return (concat,timestamp)
78 concat: concatenation of file content
79 timestamp: max(os.path.getmtime of file_list)
81 root = openerpweb.path_root
85 fname = os.path.join(root, i)
86 ftime = os.path.getmtime(fname)
87 if ftime > files_timestamp:
88 files_timestamp = ftime
89 files_content = open(fname).read()
90 files_concat = "".join(files_content)
93 @openerpweb.jsonrequest
94 def login(self, req, db, login, password):
95 req.session.login(db, login, password)
98 "session_id": req.session_id,
99 "uid": req.session._uid,
102 @openerpweb.jsonrequest
103 def sc_list(self, req):
104 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
105 req.session.eval_context(req.context))
107 @openerpweb.jsonrequest
108 def get_databases_list(self, req):
109 proxy = req.session.proxy("db")
112 return {"db_list": dbs}
114 @openerpweb.jsonrequest
115 def modules(self, req):
116 return {"modules": [name
117 for name, manifest in openerpweb.addons_manifest.iteritems()
118 if manifest.get('active', True)]}
120 @openerpweb.jsonrequest
121 def csslist(self, req, mods='base'):
122 return {'files': self.manifest_glob(mods.split(','), 'css')}
124 @openerpweb.jsonrequest
125 def jslist(self, req, mods='base'):
126 return {'files': self.manifest_glob(mods.split(','), 'js')}
128 def css(self, req, mods='base,base_hello'):
129 files = self.manifest_glob(mods.split(','), 'css')
130 concat = self.concat_files(files)[0]
131 # TODO request set the Date of last modif and Etag
135 def js(self, req, mods='base,base_hello'):
136 files = self.manifest_glob(mods.split(','), 'js')
137 concat = self.concat_files(files)[0]
138 # TODO request set the Date of last modif and Etag
142 @openerpweb.jsonrequest
143 def eval_domain_and_context(self, req, contexts, domains,
145 """ Evaluates sequences of domains and contexts, composing them into
146 a single context, domain or group_by sequence.
148 :param list contexts: list of contexts to merge together. Contexts are
149 evaluated in sequence, all previous contexts
150 are part of their own evaluation context
151 (starting at the session context).
152 :param list domains: list of domains to merge together. Domains are
153 evaluated in sequence and appended to one another
154 (implicit AND), their evaluation domain is the
155 result of merging all contexts.
156 :param list group_by_seq: list of domains (which may be in a different
157 order than the ``contexts`` parameter),
158 evaluated in sequence, their ``'group_by'``
159 key is extracted if they have one.
164 the global context created by merging all of
168 the concatenation of all domains
171 a list of fields to group by, potentially empty (in which case
172 no group by should be performed)
174 context = req.session.eval_context(openerpweb.nonliterals.CompoundContext(*contexts))
175 domain = req.session.eval_domain(openerpweb.nonliterals.CompoundDomain(*(domains or [])), context)
177 group_by_sequence = []
178 for candidate in (group_by_seq or []):
179 ctx = req.session.eval_context(candidate, context)
180 group_by = ctx.get('group_by')
183 elif isinstance(group_by, basestring):
184 group_by_sequence.append(group_by)
186 group_by_sequence.extend(group_by)
191 'group_by': group_by_sequence
194 @openerpweb.jsonrequest
195 def save_session_action(self, req, the_action):
197 This method store an action object in the session object and returns an integer
198 identifying that action. The method get_session_action() can be used to get
201 :param the_action: The action to save in the session.
202 :type the_action: anything
203 :return: A key identifying the saved action.
206 saved_actions = cherrypy.session.get('saved_actions')
207 if not saved_actions:
208 saved_actions = {"next":0, "actions":{}}
209 cherrypy.session['saved_actions'] = saved_actions
210 # we don't allow more than 10 stored actions
211 if len(saved_actions["actions"]) >= 10:
212 del saved_actions["actions"][min(saved_actions["actions"].keys())]
213 key = saved_actions["next"]
214 saved_actions["actions"][key] = the_action
215 saved_actions["next"] = key + 1
218 @openerpweb.jsonrequest
219 def get_session_action(self, req, key):
221 Gets back a previously saved action. This method can return None if the action
222 was saved since too much time (this case should be handled in a smart way).
224 :param key: The key given by save_session_action()
226 :return: The saved action or None.
229 saved_actions = cherrypy.session.get('saved_actions')
230 if not saved_actions:
232 return saved_actions["actions"].get(key)
234 def eval_context_and_domain(session, context, domain=None):
235 e_context = session.eval_context(context)
236 e_domain = session.eval_domain(domain or [], e_context)
238 return (e_context, e_domain)
240 def load_actions_from_ir_values(req, key, key2, models, meta, context):
241 Values = req.session.model('ir.values')
242 actions = Values.get(key, key2, models, meta, context)
244 for _, _, action in actions:
245 clean_action(action, req.session)
249 def clean_action(action, session):
250 # values come from the server, we can just eval them
251 if isinstance(action['context'], basestring):
252 action['context'] = eval(
254 session.evaluation_context()) or {}
256 if isinstance(action['domain'], basestring):
257 action['domain'] = eval(
259 session.evaluation_context(
260 action['context'])) or []
261 if not action.has_key('flags'):
262 # Set empty flags dictionary for web client.
263 action['flags'] = dict()
264 return fix_view_modes(action)
266 def fix_view_modes(action):
267 """ For historical reasons, OpenERP has weird dealings in relation to
268 view_mode and the view_type attribute (on window actions):
270 * one of the view modes is ``tree``, which stands for both list views
272 * the choice is made by checking ``view_type``, which is either
273 ``form`` for a list view or ``tree`` for an actual tree view
275 This methods simply folds the view_type into view_mode by adding a
276 new view mode ``list`` which is the result of the ``tree`` view_mode
277 in conjunction with the ``form`` view_type.
279 TODO: this should go into the doc, some kind of "peculiarities" section
281 :param dict action: an action descriptor
282 :returns: nothing, the action is modified in place
284 if action.pop('view_type') != 'form':
287 action['view_mode'] = ','.join(
288 mode if mode != 'tree' else 'list'
289 for mode in action['view_mode'].split(','))
291 [id, mode if mode != 'tree' else 'list']
292 for id, mode in action['views']
296 class Menu(openerpweb.Controller):
297 _cp_path = "/base/menu"
299 @openerpweb.jsonrequest
301 return {'data': self.do_load(req)}
303 def do_load(self, req):
304 """ Loads all menu items (all applications and their sub-menus).
306 :param req: A request object, with an OpenERP session attribute
307 :type req: < session -> OpenERPSession >
308 :return: the menu root
309 :rtype: dict('children': menu_nodes)
311 Menus = req.session.model('ir.ui.menu')
312 # menus are loaded fully unlike a regular tree view, cause there are
313 # less than 512 items
314 context = req.session.eval_context(req.context)
315 menu_ids = Menus.search([], 0, False, False, context)
316 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
317 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
318 menu_items.append(menu_root)
320 # make a tree using parent_id
321 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
322 for menu_item in menu_items:
323 if menu_item['parent_id']:
324 parent = menu_item['parent_id'][0]
327 if parent in menu_items_map:
328 menu_items_map[parent].setdefault(
329 'children', []).append(menu_item)
331 # sort by sequence a tree using parent_id
332 for menu_item in menu_items:
333 menu_item.setdefault('children', []).sort(
334 key=lambda x:x["sequence"])
338 @openerpweb.jsonrequest
339 def action(self, req, menu_id):
340 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
341 [('ir.ui.menu', menu_id)], False,
342 req.session.eval_context(req.context))
343 return {"action": actions}
345 class DataSet(openerpweb.Controller):
346 _cp_path = "/base/dataset"
348 @openerpweb.jsonrequest
349 def fields(self, req, model):
350 return {'fields': req.session.model(model).fields_get(False,
351 req.session.eval_context(req.context))}
353 @openerpweb.jsonrequest
354 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
355 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
356 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
357 """ Performs a search() followed by a read() (if needed) using the
358 provided search criteria
360 :param request: a JSON-RPC request object
361 :type request: openerpweb.JsonRequest
362 :param str model: the name of the model to search on
363 :param fields: a list of the fields to return in the result records
365 :param int offset: from which index should the results start being returned
366 :param int limit: the maximum number of records to return
367 :param list domain: the search domain for the query
368 :param list sort: sorting directives
369 :returns: a list of result records
372 Model = request.session.model(model)
373 context, domain = eval_context_and_domain(request.session, request.context, domain)
375 ids = Model.search(domain, offset or 0, limit or False,
376 sort or False, context)
378 if fields and fields == ['id']:
379 # shortcut read if we only want the ids
380 return map(lambda id: {'id': id}, ids)
382 reads = Model.read(ids, fields or False, context)
383 reads.sort(key=lambda obj: ids.index(obj['id']))
386 @openerpweb.jsonrequest
387 def get(self, request, model, ids, fields=False):
388 return self.do_get(request, model, ids, fields)
389 def do_get(self, request, model, ids, fields=False):
390 """ Fetches and returns the records of the model ``model`` whose ids
393 The results are in the same order as the inputs, but elements may be
394 missing (if there is no record left for the id)
396 :param request: the JSON-RPC2 request object
397 :type request: openerpweb.JsonRequest
398 :param model: the model to read from
400 :param ids: a list of identifiers
402 :param fields: a list of fields to fetch, ``False`` or empty to fetch
403 all fields in the model
404 :type fields: list | False
405 :returns: a list of records, in the same order as the list of ids
408 Model = request.session.model(model)
409 records = Model.read(ids, fields, request.session.eval_context(request.context))
411 record_map = dict((record['id'], record) for record in records)
413 return [record_map[id] for id in ids if record_map.get(id)]
415 @openerpweb.jsonrequest
416 def load(self, req, model, id, fields):
417 m = req.session.model(model)
419 r = m.read([id], False, req.session.eval_context(req.context))
422 return {'value': value}
424 @openerpweb.jsonrequest
425 def create(self, req, model, data):
426 m = req.session.model(model)
427 r = m.create(data, req.session.eval_context(req.context))
430 @openerpweb.jsonrequest
431 def save(self, req, model, id, data):
432 m = req.session.model(model)
433 r = m.write([id], data, req.session.eval_context(req.context))
436 @openerpweb.jsonrequest
437 def unlink(self, request, model, ids=[]):
438 Model = request.session.model(model)
439 return Model.unlink(ids, request.session.eval_context(request.context))
441 @openerpweb.jsonrequest
442 def call(self, req, model, method, args, domain_id=None, context_id=None):
443 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
444 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
445 c, d = eval_context_and_domain(req.session, context, domain);
446 if(domain_id and len(args) - 1 >= domain_id):
448 if(context_id and len(args) - 1 >= context_id):
451 m = req.session.model(model)
452 r = getattr(m, method)(*args)
455 @openerpweb.jsonrequest
456 def exec_workflow(self, req, model, id, signal):
457 r = req.session.exec_workflow(model, id, signal)
460 @openerpweb.jsonrequest
461 def default_get(self, req, model, fields):
462 m = req.session.model(model)
463 r = m.default_get(fields, req.session.eval_context(req.context))
466 class DataGroup(openerpweb.Controller):
467 _cp_path = "/base/group"
468 @openerpweb.jsonrequest
469 def read(self, request, model, group_by_fields, domain=None):
470 Model = request.session.model(model)
471 context, domain = eval_context_and_domain(request.session, request.context, domain)
473 return Model.read_group(
474 domain or [], False, group_by_fields, 0, False,
475 dict(context, group_by=group_by_fields))
477 class View(openerpweb.Controller):
478 _cp_path = "/base/view"
480 def fields_view_get(self, request, model, view_id, view_type,
481 transform=True, toolbar=False, submenu=False):
482 Model = request.session.model(model)
483 context = request.session.eval_context(request.context)
484 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
485 # todo fme?: check that we should pass the evaluated context here
486 self.process_view(request.session, fvg, context, transform)
489 def process_view(self, session, fvg, context, transform):
491 evaluation_context = session.evaluation_context(context or {})
492 xml = self.transform_view(fvg['arch'], session, evaluation_context)
494 xml = ElementTree.fromstring(fvg['arch'])
495 fvg['arch'] = Xml2Json.convert_element(xml)
496 for field in fvg['fields'].values():
497 if field.has_key('views') and field['views']:
498 for view in field["views"].values():
499 self.process_view(session, view, None, transform)
501 @openerpweb.jsonrequest
502 def add_custom(self, request, view_id, arch):
503 CustomView = request.session.model('ir.ui.view.custom')
505 'user_id': request.session._uid,
508 }, request.session.eval_context(request.context))
509 return {'result': True}
511 @openerpweb.jsonrequest
512 def undo_custom(self, request, view_id, reset=False):
513 CustomView = request.session.model('ir.ui.view.custom')
514 context = request.session.eval_context(request.context)
515 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
516 0, False, False, context)
519 CustomView.unlink(vcustom, context)
521 CustomView.unlink([vcustom[0]], context)
522 return {'result': True}
523 return {'result': False}
525 def normalize_attrs(self, elem, context):
526 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
527 the client only has to deal with @attrs.
529 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
532 :param elem: the current view node (Python object)
533 :type elem: xml.etree.ElementTree.Element
534 :param dict context: evaluation context
536 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
537 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
538 if 'states' in elem.attrib:
539 attrs.setdefault('invisible', [])\
540 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
542 elem.set('attrs', simplejson.dumps(attrs))
543 for a in ['invisible', 'readonly', 'required']:
545 # In the XML we trust
546 avalue = bool(eval(elem.get(a, 'False'),
547 {'context': context or {}}))
552 if a == 'invisible' and 'attrs' in elem.attrib:
553 del elem.attrib['attrs']
555 def transform_view(self, view_string, session, context=None):
556 # transform nodes on the fly via iterparse, instead of
557 # doing it statically on the parsing result
558 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
560 for event, elem in parser:
564 self.normalize_attrs(elem, context)
565 self.parse_domains_and_contexts(elem, session)
568 def parse_domain(self, elem, attr_name, session):
569 """ Parses an attribute of the provided name as a domain, transforms it
570 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
572 :param elem: the node being parsed
573 :type param: xml.etree.ElementTree.Element
574 :param str attr_name: the name of the attribute which should be parsed
575 :param session: Current OpenERP session
576 :type session: openerpweb.openerpweb.OpenERPSession
578 domain = elem.get(attr_name, '').strip()
583 openerpweb.ast.literal_eval(
588 openerpweb.nonliterals.Domain(session, domain))
590 def parse_domains_and_contexts(self, elem, session):
591 """ Converts domains and contexts from the view into Python objects,
592 either literals if they can be parsed by literal_eval or a special
593 placeholder object if the domain or context refers to free variables.
595 :param elem: the current node being parsed
596 :type param: xml.etree.ElementTree.Element
597 :param session: OpenERP session object, used to store and retrieve
599 :type session: openerpweb.openerpweb.OpenERPSession
601 self.parse_domain(elem, 'domain', session)
602 self.parse_domain(elem, 'filter_domain', session)
603 context_string = elem.get('context', '').strip()
607 openerpweb.ast.literal_eval(context_string))
610 openerpweb.nonliterals.Context(
611 session, context_string))
613 class FormView(View):
614 _cp_path = "/base/formview"
616 @openerpweb.jsonrequest
617 def load(self, req, model, view_id, toolbar=False):
618 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
619 return {'fields_view': fields_view}
621 class ListView(View):
622 _cp_path = "/base/listview"
624 @openerpweb.jsonrequest
625 def load(self, req, model, view_id, toolbar=False):
626 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
627 return {'fields_view': fields_view}
629 def process_colors(self, view, row, context):
630 colors = view['arch']['attrs'].get('colors')
637 for pair in colors.split(';')
638 if eval(pair.split(':')[1], dict(context, **row))
643 elif len(color) == 1:
647 class SearchView(View):
648 _cp_path = "/base/searchview"
650 @openerpweb.jsonrequest
651 def load(self, req, model, view_id):
652 fields_view = self.fields_view_get(req, model, view_id, 'search')
653 return {'fields_view': fields_view}
655 @openerpweb.jsonrequest
656 def fields_get(self, req, model):
657 Model = req.session.model(model)
658 fields = Model.fields_get(False, req.session.eval_context(req.context))
659 return {'fields': fields}
661 class Binary(openerpweb.Controller):
662 _cp_path = "/base/binary"
664 @openerpweb.httprequest
665 def image(self, request, session_id, model, id, field, **kw):
666 cherrypy.response.headers['Content-Type'] = 'image/png'
667 Model = request.session.model(model)
668 context = request.session.eval_context(request.context)
671 res = Model.default_get([field], context).get(field, '')
673 res = Model.read([int(id)], [field], context)[0].get(field, '')
674 return base64.decodestring(res)
675 except: # TODO: what's the exception here?
676 return self.placeholder()
677 def placeholder(self):
678 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
680 @openerpweb.httprequest
681 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
682 Model = request.session.model(model)
683 context = request.session.eval_context(request.context)
684 res = Model.read([int(id)], [field, fieldname], context)[0]
685 filecontent = res.get(field, '')
687 raise cherrypy.NotFound
689 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
690 filename = '%s_%s' % (model.replace('.', '_'), id)
692 filename = res.get(fieldname, '') or filename
693 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
694 return base64.decodestring(filecontent)
696 @openerpweb.httprequest
697 def upload(self, request, session_id, callback, ufile=None):
698 cherrypy.response.timeout = 500
700 for key, val in cherrypy.request.headers.iteritems():
701 headers[key.lower()] = val
702 size = int(headers.get('content-length', 0))
703 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
705 out = """<script language="javascript" type="text/javascript">
706 var win = window.top.window,
708 if (typeof(callback) === 'function') {
709 callback.apply(this, %s);
711 win.jQuery('#oe_notification', win.document).notify('create', {
712 title: "Ajax File Upload",
713 text: "Could not find callback"
717 data = ufile.file.read()
718 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
720 args = [False, e.message]
721 return out % (simplejson.dumps(callback), simplejson.dumps(args))
723 @openerpweb.httprequest
724 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
725 cherrypy.response.timeout = 500
726 context = request.session.eval_context(request.context)
727 Model = request.session.model('ir.attachment')
729 out = """<script language="javascript" type="text/javascript">
730 var win = window.top.window,
732 if (typeof(callback) === 'function') {
733 callback.call(this, %s);
736 attachment_id = Model.create({
737 'name': ufile.filename,
738 'datas': base64.encodestring(ufile.file.read()),
743 'filename': ufile.filename,
747 args = { 'error': e.message }
748 return out % (simplejson.dumps(callback), simplejson.dumps(args))
750 class Action(openerpweb.Controller):
751 _cp_path = "/base/action"
753 @openerpweb.jsonrequest
754 def load(self, req, action_id):
755 Actions = req.session.model('ir.actions.actions')
757 context = req.session.eval_context(req.context)
758 action_type = Actions.read([action_id], ['type'], context)
760 action = req.session.model(action_type[0]['type']).read([action_id], False,
763 value = clean_action(action[0], req.session)
764 return {'result': value}