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 contexts = contexts or []
175 domains = domains or []
176 e_context = dict(reduce(lambda x, y: x + y, [openerpweb.nonliterals.get_eval_context(x).items() for x in contexts]))
177 context, domain = eval_context_and_domain(req.session,
178 openerpweb.nonliterals.CompoundContext(*contexts).set_eval_context(e_context),
179 openerpweb.nonliterals.CompoundDomain(*domains))
181 group_by_sequence = []
182 for candidate in (group_by_seq or []):
183 ctx = req.session.eval_context(candidate, context)
184 group_by = ctx.get('group_by')
187 elif isinstance(group_by, basestring):
188 group_by_sequence.append(group_by)
190 group_by_sequence.extend(group_by)
195 'group_by': group_by_sequence
198 @openerpweb.jsonrequest
199 def save_session_action(self, req, the_action):
201 This method store an action object in the session object and returns an integer
202 identifying that action. The method get_session_action() can be used to get
205 :param the_action: The action to save in the session.
206 :type the_action: anything
207 :return: A key identifying the saved action.
210 saved_actions = cherrypy.session.get('saved_actions')
211 if not saved_actions:
212 saved_actions = {"next":0, "actions":{}}
213 cherrypy.session['saved_actions'] = saved_actions
214 # we don't allow more than 10 stored actions
215 if len(saved_actions["actions"]) >= 10:
216 del saved_actions["actions"][min(saved_actions["actions"].keys())]
217 key = saved_actions["next"]
218 saved_actions["actions"][key] = the_action
219 saved_actions["next"] = key + 1
222 @openerpweb.jsonrequest
223 def get_session_action(self, req, key):
225 Gets back a previously saved action. This method can return None if the action
226 was saved since too much time (this case should be handled in a smart way).
228 :param key: The key given by save_session_action()
230 :return: The saved action or None.
233 saved_actions = cherrypy.session.get('saved_actions')
234 if not saved_actions:
236 return saved_actions["actions"].get(key)
238 def eval_context_and_domain(session, context, domain=None):
239 e_context = session.eval_context(context)
240 eval_context = openerpweb.nonliterals.get_eval_context(context)
241 e_domain = session.eval_domain(domain or [], dict(eval_context.items() + e_context.items()))
243 return (e_context, e_domain)
245 def load_actions_from_ir_values(req, key, key2, models, meta, context):
246 Values = req.session.model('ir.values')
247 actions = Values.get(key, key2, models, meta, context)
249 for _, _, action in actions:
250 clean_action(action, req.session)
254 def clean_action(action, session):
255 # values come from the server, we can just eval them
256 if isinstance(action['context'], basestring):
257 action['context'] = eval(
259 session.evaluation_context()) or {}
261 if isinstance(action['domain'], basestring):
262 action['domain'] = eval(
264 session.evaluation_context(
265 action['context'])) or []
266 if not action.has_key('flags'):
267 # Set empty flags dictionary for web client.
268 action['flags'] = dict()
269 return fix_view_modes(action)
271 def fix_view_modes(action):
272 """ For historical reasons, OpenERP has weird dealings in relation to
273 view_mode and the view_type attribute (on window actions):
275 * one of the view modes is ``tree``, which stands for both list views
277 * the choice is made by checking ``view_type``, which is either
278 ``form`` for a list view or ``tree`` for an actual tree view
280 This methods simply folds the view_type into view_mode by adding a
281 new view mode ``list`` which is the result of the ``tree`` view_mode
282 in conjunction with the ``form`` view_type.
284 TODO: this should go into the doc, some kind of "peculiarities" section
286 :param dict action: an action descriptor
287 :returns: nothing, the action is modified in place
289 if action.pop('view_type') != 'form':
292 action['view_mode'] = ','.join(
293 mode if mode != 'tree' else 'list'
294 for mode in action['view_mode'].split(','))
296 [id, mode if mode != 'tree' else 'list']
297 for id, mode in action['views']
301 class Menu(openerpweb.Controller):
302 _cp_path = "/base/menu"
304 @openerpweb.jsonrequest
306 return {'data': self.do_load(req)}
308 def do_load(self, req):
309 """ Loads all menu items (all applications and their sub-menus).
311 :param req: A request object, with an OpenERP session attribute
312 :type req: < session -> OpenERPSession >
313 :return: the menu root
314 :rtype: dict('children': menu_nodes)
316 Menus = req.session.model('ir.ui.menu')
317 # menus are loaded fully unlike a regular tree view, cause there are
318 # less than 512 items
319 context = req.session.eval_context(req.context)
320 menu_ids = Menus.search([], 0, False, False, context)
321 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
322 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
323 menu_items.append(menu_root)
325 # make a tree using parent_id
326 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
327 for menu_item in menu_items:
328 if menu_item['parent_id']:
329 parent = menu_item['parent_id'][0]
332 if parent in menu_items_map:
333 menu_items_map[parent].setdefault(
334 'children', []).append(menu_item)
336 # sort by sequence a tree using parent_id
337 for menu_item in menu_items:
338 menu_item.setdefault('children', []).sort(
339 key=lambda x:x["sequence"])
343 @openerpweb.jsonrequest
344 def action(self, req, menu_id):
345 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
346 [('ir.ui.menu', menu_id)], False,
347 req.session.eval_context(req.context))
348 return {"action": actions}
350 class DataSet(openerpweb.Controller):
351 _cp_path = "/base/dataset"
353 @openerpweb.jsonrequest
354 def fields(self, req, model):
355 return {'fields': req.session.model(model).fields_get(False,
356 req.session.eval_context(req.context))}
358 @openerpweb.jsonrequest
359 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
360 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
361 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
362 """ Performs a search() followed by a read() (if needed) using the
363 provided search criteria
365 :param request: a JSON-RPC request object
366 :type request: openerpweb.JsonRequest
367 :param str model: the name of the model to search on
368 :param fields: a list of the fields to return in the result records
370 :param int offset: from which index should the results start being returned
371 :param int limit: the maximum number of records to return
372 :param list domain: the search domain for the query
373 :param list sort: sorting directives
374 :returns: a list of result records
377 Model = request.session.model(model)
378 context, domain = eval_context_and_domain(request.session, request.context, domain)
380 ids = Model.search(domain, offset or 0, limit or False,
381 sort or False, context)
383 if fields and fields == ['id']:
384 # shortcut read if we only want the ids
385 return map(lambda id: {'id': id}, ids)
387 reads = Model.read(ids, fields or False, context)
388 reads.sort(key=lambda obj: ids.index(obj['id']))
391 @openerpweb.jsonrequest
392 def get(self, request, model, ids, fields=False):
393 return self.do_get(request, model, ids, fields)
394 def do_get(self, request, model, ids, fields=False):
395 """ Fetches and returns the records of the model ``model`` whose ids
398 The results are in the same order as the inputs, but elements may be
399 missing (if there is no record left for the id)
401 :param request: the JSON-RPC2 request object
402 :type request: openerpweb.JsonRequest
403 :param model: the model to read from
405 :param ids: a list of identifiers
407 :param fields: a list of fields to fetch, ``False`` or empty to fetch
408 all fields in the model
409 :type fields: list | False
410 :returns: a list of records, in the same order as the list of ids
413 Model = request.session.model(model)
414 records = Model.read(ids, fields, request.session.eval_context(request.context))
416 record_map = dict((record['id'], record) for record in records)
418 return [record_map[id] for id in ids if record_map.get(id)]
420 @openerpweb.jsonrequest
421 def load(self, req, model, id, fields):
422 m = req.session.model(model)
424 r = m.read([id], False, req.session.eval_context(req.context))
427 return {'value': value}
429 @openerpweb.jsonrequest
430 def create(self, req, model, data):
431 m = req.session.model(model)
432 r = m.create(data, req.session.eval_context(req.context))
435 @openerpweb.jsonrequest
436 def save(self, req, model, id, data):
437 m = req.session.model(model)
438 r = m.write([id], data, req.session.eval_context(req.context))
441 @openerpweb.jsonrequest
442 def unlink(self, request, model, ids=[]):
443 Model = request.session.model(model)
444 return Model.unlink(ids, request.session.eval_context(request.context))
446 @openerpweb.jsonrequest
447 def call(self, req, model, method, args, domain_id=None, context_id=None):
448 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
449 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
450 c, d = eval_context_and_domain(req.session, context, domain);
451 if(domain_id and len(args) - 1 >= domain_id):
453 if(context_id and len(args) - 1 >= context_id):
456 m = req.session.model(model)
457 r = getattr(m, method)(*args)
460 @openerpweb.jsonrequest
461 def exec_workflow(self, req, model, id, signal):
462 r = req.session.exec_workflow(model, id, signal)
465 @openerpweb.jsonrequest
466 def default_get(self, req, model, fields):
467 m = req.session.model(model)
468 r = m.default_get(fields, req.session.eval_context(req.context))
471 class DataGroup(openerpweb.Controller):
472 _cp_path = "/base/group"
473 @openerpweb.jsonrequest
474 def read(self, request, model, group_by_fields, domain=None):
475 Model = request.session.model(model)
476 context, domain = eval_context_and_domain(request.session, request.context, domain)
478 return Model.read_group(
479 domain or [], False, group_by_fields, 0, False,
480 dict(context, group_by=group_by_fields))
482 class View(openerpweb.Controller):
483 _cp_path = "/base/view"
485 def fields_view_get(self, request, model, view_id, view_type,
486 transform=True, toolbar=False, submenu=False):
487 Model = request.session.model(model)
488 context = request.session.eval_context(request.context)
489 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
490 # todo fme?: check that we should pass the evaluated context here
491 self.process_view(request.session, fvg, context, transform)
494 def process_view(self, session, fvg, context, transform):
496 evaluation_context = session.evaluation_context(context or {})
497 xml = self.transform_view(fvg['arch'], session, evaluation_context)
499 xml = ElementTree.fromstring(fvg['arch'])
500 fvg['arch'] = Xml2Json.convert_element(xml)
501 for field in fvg['fields'].values():
502 if field.has_key('views') and field['views']:
503 for view in field["views"].values():
504 self.process_view(session, view, None, transform)
506 @openerpweb.jsonrequest
507 def add_custom(self, request, view_id, arch):
508 CustomView = request.session.model('ir.ui.view.custom')
510 'user_id': request.session._uid,
513 }, request.session.eval_context(request.context))
514 return {'result': True}
516 @openerpweb.jsonrequest
517 def undo_custom(self, request, view_id, reset=False):
518 CustomView = request.session.model('ir.ui.view.custom')
519 context = request.session.eval_context(request.context)
520 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
521 0, False, False, context)
524 CustomView.unlink(vcustom, context)
526 CustomView.unlink([vcustom[0]], context)
527 return {'result': True}
528 return {'result': False}
530 def normalize_attrs(self, elem, context):
531 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
532 the client only has to deal with @attrs.
534 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
537 :param elem: the current view node (Python object)
538 :type elem: xml.etree.ElementTree.Element
539 :param dict context: evaluation context
541 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
542 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
543 if 'states' in elem.attrib:
544 attrs.setdefault('invisible', [])\
545 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
547 elem.set('attrs', simplejson.dumps(attrs))
548 for a in ['invisible', 'readonly', 'required']:
550 # In the XML we trust
551 avalue = bool(eval(elem.get(a, 'False'),
552 {'context': context or {}}))
557 if a == 'invisible' and 'attrs' in elem.attrib:
558 del elem.attrib['attrs']
560 def transform_view(self, view_string, session, context=None):
561 # transform nodes on the fly via iterparse, instead of
562 # doing it statically on the parsing result
563 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
565 for event, elem in parser:
569 self.normalize_attrs(elem, context)
570 self.parse_domains_and_contexts(elem, session)
573 def parse_domain(self, elem, attr_name, session):
574 """ Parses an attribute of the provided name as a domain, transforms it
575 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
577 :param elem: the node being parsed
578 :type param: xml.etree.ElementTree.Element
579 :param str attr_name: the name of the attribute which should be parsed
580 :param session: Current OpenERP session
581 :type session: openerpweb.openerpweb.OpenERPSession
583 domain = elem.get(attr_name, '').strip()
588 openerpweb.ast.literal_eval(
593 openerpweb.nonliterals.Domain(session, domain))
595 def parse_domains_and_contexts(self, elem, session):
596 """ Converts domains and contexts from the view into Python objects,
597 either literals if they can be parsed by literal_eval or a special
598 placeholder object if the domain or context refers to free variables.
600 :param elem: the current node being parsed
601 :type param: xml.etree.ElementTree.Element
602 :param session: OpenERP session object, used to store and retrieve
604 :type session: openerpweb.openerpweb.OpenERPSession
606 self.parse_domain(elem, 'domain', session)
607 self.parse_domain(elem, 'filter_domain', session)
608 context_string = elem.get('context', '').strip()
612 openerpweb.ast.literal_eval(context_string))
615 openerpweb.nonliterals.Context(
616 session, context_string))
618 class FormView(View):
619 _cp_path = "/base/formview"
621 @openerpweb.jsonrequest
622 def load(self, req, model, view_id, toolbar=False):
623 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
624 return {'fields_view': fields_view}
626 class ListView(View):
627 _cp_path = "/base/listview"
629 @openerpweb.jsonrequest
630 def load(self, req, model, view_id, toolbar=False):
631 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
632 return {'fields_view': fields_view}
634 def process_colors(self, view, row, context):
635 colors = view['arch']['attrs'].get('colors')
642 for pair in colors.split(';')
643 if eval(pair.split(':')[1], dict(context, **row))
648 elif len(color) == 1:
652 class SearchView(View):
653 _cp_path = "/base/searchview"
655 @openerpweb.jsonrequest
656 def load(self, req, model, view_id):
657 fields_view = self.fields_view_get(req, model, view_id, 'search')
658 return {'fields_view': fields_view}
660 @openerpweb.jsonrequest
661 def fields_get(self, req, model):
662 Model = req.session.model(model)
663 fields = Model.fields_get(False, req.session.eval_context(req.context))
664 return {'fields': fields}
666 class Binary(openerpweb.Controller):
667 _cp_path = "/base/binary"
669 @openerpweb.httprequest
670 def image(self, request, session_id, model, id, field, **kw):
671 cherrypy.response.headers['Content-Type'] = 'image/png'
672 Model = request.session.model(model)
673 context = request.session.eval_context(request.context)
676 res = Model.default_get([field], context).get(field, '')
678 res = Model.read([int(id)], [field], context)[0].get(field, '')
679 return base64.decodestring(res)
680 except: # TODO: what's the exception here?
681 return self.placeholder()
682 def placeholder(self):
683 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
685 @openerpweb.httprequest
686 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
687 Model = request.session.model(model)
688 context = request.session.eval_context(request.context)
689 res = Model.read([int(id)], [field, fieldname], context)[0]
690 filecontent = res.get(field, '')
692 raise cherrypy.NotFound
694 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
695 filename = '%s_%s' % (model.replace('.', '_'), id)
697 filename = res.get(fieldname, '') or filename
698 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
699 return base64.decodestring(filecontent)
701 @openerpweb.httprequest
702 def upload(self, request, session_id, callback, ufile=None):
703 cherrypy.response.timeout = 500
705 for key, val in cherrypy.request.headers.iteritems():
706 headers[key.lower()] = val
707 size = int(headers.get('content-length', 0))
708 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
710 out = """<script language="javascript" type="text/javascript">
711 var win = window.top.window,
713 if (typeof(callback) === 'function') {
714 callback.apply(this, %s);
716 win.jQuery('#oe_notification', win.document).notify('create', {
717 title: "Ajax File Upload",
718 text: "Could not find callback"
722 data = ufile.file.read()
723 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
725 args = [False, e.message]
726 return out % (simplejson.dumps(callback), simplejson.dumps(args))
728 @openerpweb.httprequest
729 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
730 cherrypy.response.timeout = 500
731 context = request.session.eval_context(request.context)
732 Model = request.session.model('ir.attachment')
734 out = """<script language="javascript" type="text/javascript">
735 var win = window.top.window,
737 if (typeof(callback) === 'function') {
738 callback.call(this, %s);
741 attachment_id = Model.create({
742 'name': ufile.filename,
743 'datas': base64.encodestring(ufile.file.read()),
748 'filename': ufile.filename,
752 args = { 'error': e.message }
753 return out % (simplejson.dumps(callback), simplejson.dumps(args))
755 class Action(openerpweb.Controller):
756 _cp_path = "/base/action"
758 @openerpweb.jsonrequest
759 def load(self, req, action_id):
760 Actions = req.session.model('ir.actions.actions')
762 context = req.session.eval_context(req.context)
763 action_type = Actions.read([action_id], ['type'], context)
765 action = req.session.model(action_type[0]['type']).read([action_id], False,
768 value = clean_action(action[0], req.session)
769 return {'result': value}