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, domain = eval_context_and_domain(req.session,
175 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
176 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
178 group_by_sequence = []
179 for candidate in (group_by_seq or []):
180 ctx = req.session.eval_context(candidate, context)
181 group_by = ctx.get('group_by')
184 elif isinstance(group_by, basestring):
185 group_by_sequence.append(group_by)
187 group_by_sequence.extend(group_by)
192 'group_by': group_by_sequence
195 @openerpweb.jsonrequest
196 def save_session_action(self, req, the_action):
198 This method store an action object in the session object and returns an integer
199 identifying that action. The method get_session_action() can be used to get
202 :param the_action: The action to save in the session.
203 :type the_action: anything
204 :return: A key identifying the saved action.
207 saved_actions = cherrypy.session.get('saved_actions')
208 if not saved_actions:
209 saved_actions = {"next":0, "actions":{}}
210 cherrypy.session['saved_actions'] = saved_actions
211 # we don't allow more than 10 stored actions
212 if len(saved_actions["actions"]) >= 10:
213 del saved_actions["actions"][min(saved_actions["actions"].keys())]
214 key = saved_actions["next"]
215 saved_actions["actions"][key] = the_action
216 saved_actions["next"] = key + 1
219 @openerpweb.jsonrequest
220 def get_session_action(self, req, key):
222 Gets back a previously saved action. This method can return None if the action
223 was saved since too much time (this case should be handled in a smart way).
225 :param key: The key given by save_session_action()
227 :return: The saved action or None.
230 saved_actions = cherrypy.session.get('saved_actions')
231 if not saved_actions:
233 return saved_actions["actions"].get(key)
235 def eval_context_and_domain(session, context, domain=None):
236 e_context = session.eval_context(context)
237 # should we give the evaluated context as an evaluation context to the domain?
238 e_domain = session.eval_domain(domain or [])
240 return e_context, e_domain
242 def load_actions_from_ir_values(req, key, key2, models, meta, context):
243 Values = req.session.model('ir.values')
244 actions = Values.get(key, key2, models, meta, context)
246 return [(id, name, clean_action(action, req.session))
247 for id, name, action in actions]
249 def clean_action(action, session):
250 if action['type'] != 'ir.actions.act_window':
252 # values come from the server, we can just eval them
253 if isinstance(action.get('context', None), basestring):
254 action['context'] = eval(
256 session.evaluation_context()) or {}
258 if isinstance(action.get('domain', None), basestring):
259 action['domain'] = eval(
261 session.evaluation_context(
262 action['context'])) or []
263 if 'flags' not in action:
264 # Set empty flags dictionary for web client.
265 action['flags'] = dict()
266 return fix_view_modes(action)
268 def fix_view_modes(action):
269 """ For historical reasons, OpenERP has weird dealings in relation to
270 view_mode and the view_type attribute (on window actions):
272 * one of the view modes is ``tree``, which stands for both list views
274 * the choice is made by checking ``view_type``, which is either
275 ``form`` for a list view or ``tree`` for an actual tree view
277 This methods simply folds the view_type into view_mode by adding a
278 new view mode ``list`` which is the result of the ``tree`` view_mode
279 in conjunction with the ``form`` view_type.
281 TODO: this should go into the doc, some kind of "peculiarities" section
283 :param dict action: an action descriptor
284 :returns: nothing, the action is modified in place
286 if action.pop('view_type') != 'form':
289 if 'view_mode' in action:
290 action['view_mode'] = ','.join(
291 mode if mode != 'tree' else 'list'
292 for mode in action['view_mode'].split(','))
293 if 'views' in action:
295 [id, mode if mode != 'tree' else 'list']
296 for id, mode in action['views']
300 class Menu(openerpweb.Controller):
301 _cp_path = "/base/menu"
303 @openerpweb.jsonrequest
305 return {'data': self.do_load(req)}
307 def do_load(self, req):
308 """ Loads all menu items (all applications and their sub-menus).
310 :param req: A request object, with an OpenERP session attribute
311 :type req: < session -> OpenERPSession >
312 :return: the menu root
313 :rtype: dict('children': menu_nodes)
315 Menus = req.session.model('ir.ui.menu')
316 # menus are loaded fully unlike a regular tree view, cause there are
317 # less than 512 items
318 context = req.session.eval_context(req.context)
319 menu_ids = Menus.search([], 0, False, False, context)
320 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
321 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
322 menu_items.append(menu_root)
324 # make a tree using parent_id
325 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
326 for menu_item in menu_items:
327 if menu_item['parent_id']:
328 parent = menu_item['parent_id'][0]
331 if parent in menu_items_map:
332 menu_items_map[parent].setdefault(
333 'children', []).append(menu_item)
335 # sort by sequence a tree using parent_id
336 for menu_item in menu_items:
337 menu_item.setdefault('children', []).sort(
338 key=lambda x:x["sequence"])
342 @openerpweb.jsonrequest
343 def action(self, req, menu_id):
344 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
345 [('ir.ui.menu', menu_id)], False,
346 req.session.eval_context(req.context))
347 return {"action": actions}
349 class DataSet(openerpweb.Controller):
350 _cp_path = "/base/dataset"
352 @openerpweb.jsonrequest
353 def fields(self, req, model):
354 return {'fields': req.session.model(model).fields_get(False,
355 req.session.eval_context(req.context))}
357 @openerpweb.jsonrequest
358 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
359 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
360 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
361 """ Performs a search() followed by a read() (if needed) using the
362 provided search criteria
364 :param request: a JSON-RPC request object
365 :type request: openerpweb.JsonRequest
366 :param str model: the name of the model to search on
367 :param fields: a list of the fields to return in the result records
369 :param int offset: from which index should the results start being returned
370 :param int limit: the maximum number of records to return
371 :param list domain: the search domain for the query
372 :param list sort: sorting directives
373 :returns: a list of result records
376 Model = request.session.model(model)
377 context, domain = eval_context_and_domain(request.session, request.context, domain)
379 ids = Model.search(domain, offset or 0, limit or False,
380 sort or False, context)
382 if fields and fields == ['id']:
383 # shortcut read if we only want the ids
384 return map(lambda id: {'id': id}, ids)
386 reads = Model.read(ids, fields or False, context)
387 reads.sort(key=lambda obj: ids.index(obj['id']))
390 @openerpweb.jsonrequest
391 def get(self, request, model, ids, fields=False):
392 return self.do_get(request, model, ids, fields)
393 def do_get(self, request, model, ids, fields=False):
394 """ Fetches and returns the records of the model ``model`` whose ids
397 The results are in the same order as the inputs, but elements may be
398 missing (if there is no record left for the id)
400 :param request: the JSON-RPC2 request object
401 :type request: openerpweb.JsonRequest
402 :param model: the model to read from
404 :param ids: a list of identifiers
406 :param fields: a list of fields to fetch, ``False`` or empty to fetch
407 all fields in the model
408 :type fields: list | False
409 :returns: a list of records, in the same order as the list of ids
412 Model = request.session.model(model)
413 records = Model.read(ids, fields, request.session.eval_context(request.context))
415 record_map = dict((record['id'], record) for record in records)
417 return [record_map[id] for id in ids if record_map.get(id)]
419 @openerpweb.jsonrequest
420 def load(self, req, model, id, fields):
421 m = req.session.model(model)
423 r = m.read([id], False, req.session.eval_context(req.context))
426 return {'value': value}
428 @openerpweb.jsonrequest
429 def create(self, req, model, data):
430 m = req.session.model(model)
431 r = m.create(data, req.session.eval_context(req.context))
434 @openerpweb.jsonrequest
435 def save(self, req, model, id, data):
436 m = req.session.model(model)
437 r = m.write([id], data, req.session.eval_context(req.context))
440 @openerpweb.jsonrequest
441 def unlink(self, request, model, ids=()):
442 Model = request.session.model(model)
443 return Model.unlink(ids, request.session.eval_context(request.context))
445 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
446 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
447 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
448 c, d = eval_context_and_domain(req.session, context, domain)
449 if domain_id and len(args) - 1 >= domain_id:
451 if context_id and len(args) - 1 >= context_id:
454 return getattr(req.session.model(model), method)(*args)
456 @openerpweb.jsonrequest
457 def call(self, req, model, method, args, domain_id=None, context_id=None):
458 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
460 @openerpweb.jsonrequest
461 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
462 action = self.call_common(req, model, method, args, domain_id, context_id)
463 if isinstance(action, dict) and action.get('type') != '':
464 return {'result': clean_action(action, req.session)}
465 return {'result': False}
467 @openerpweb.jsonrequest
468 def exec_workflow(self, req, model, id, signal):
469 r = req.session.exec_workflow(model, id, signal)
472 @openerpweb.jsonrequest
473 def default_get(self, req, model, fields):
474 m = req.session.model(model)
475 r = m.default_get(fields, req.session.eval_context(req.context))
478 class DataGroup(openerpweb.Controller):
479 _cp_path = "/base/group"
480 @openerpweb.jsonrequest
481 def read(self, request, model, group_by_fields, domain=None):
482 Model = request.session.model(model)
483 context, domain = eval_context_and_domain(request.session, request.context, domain)
485 return Model.read_group(
486 domain or [], False, group_by_fields, 0, False,
487 dict(context, group_by=group_by_fields))
489 class View(openerpweb.Controller):
490 _cp_path = "/base/view"
492 def fields_view_get(self, request, model, view_id, view_type,
493 transform=True, toolbar=False, submenu=False):
494 Model = request.session.model(model)
495 context = request.session.eval_context(request.context)
496 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
497 # todo fme?: check that we should pass the evaluated context here
498 self.process_view(request.session, fvg, context, transform)
501 def process_view(self, session, fvg, context, transform):
503 evaluation_context = session.evaluation_context(context or {})
504 xml = self.transform_view(fvg['arch'], session, evaluation_context)
506 xml = ElementTree.fromstring(fvg['arch'])
507 fvg['arch'] = Xml2Json.convert_element(xml)
508 for field in fvg['fields'].values():
509 if field.has_key('views') and field['views']:
510 for view in field["views"].values():
511 self.process_view(session, view, None, transform)
513 @openerpweb.jsonrequest
514 def add_custom(self, request, view_id, arch):
515 CustomView = request.session.model('ir.ui.view.custom')
517 'user_id': request.session._uid,
520 }, request.session.eval_context(request.context))
521 return {'result': True}
523 @openerpweb.jsonrequest
524 def undo_custom(self, request, view_id, reset=False):
525 CustomView = request.session.model('ir.ui.view.custom')
526 context = request.session.eval_context(request.context)
527 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
528 0, False, False, context)
531 CustomView.unlink(vcustom, context)
533 CustomView.unlink([vcustom[0]], context)
534 return {'result': True}
535 return {'result': False}
537 def normalize_attrs(self, elem, context):
538 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
539 the client only has to deal with @attrs.
541 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
544 :param elem: the current view node (Python object)
545 :type elem: xml.etree.ElementTree.Element
546 :param dict context: evaluation context
548 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
549 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
550 if 'states' in elem.attrib:
551 attrs.setdefault('invisible', [])\
552 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
554 elem.set('attrs', simplejson.dumps(attrs))
555 for a in ['invisible', 'readonly', 'required']:
557 # In the XML we trust
558 avalue = bool(eval(elem.get(a, 'False'),
559 {'context': context or {}}))
564 if a == 'invisible' and 'attrs' in elem.attrib:
565 del elem.attrib['attrs']
567 def transform_view(self, view_string, session, context=None):
568 # transform nodes on the fly via iterparse, instead of
569 # doing it statically on the parsing result
570 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
572 for event, elem in parser:
576 self.normalize_attrs(elem, context)
577 self.parse_domains_and_contexts(elem, session)
580 def parse_domain(self, elem, attr_name, session):
581 """ Parses an attribute of the provided name as a domain, transforms it
582 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
584 :param elem: the node being parsed
585 :type param: xml.etree.ElementTree.Element
586 :param str attr_name: the name of the attribute which should be parsed
587 :param session: Current OpenERP session
588 :type session: openerpweb.openerpweb.OpenERPSession
590 domain = elem.get(attr_name, '').strip()
595 openerpweb.ast.literal_eval(
600 openerpweb.nonliterals.Domain(session, domain))
602 def parse_domains_and_contexts(self, elem, session):
603 """ Converts domains and contexts from the view into Python objects,
604 either literals if they can be parsed by literal_eval or a special
605 placeholder object if the domain or context refers to free variables.
607 :param elem: the current node being parsed
608 :type param: xml.etree.ElementTree.Element
609 :param session: OpenERP session object, used to store and retrieve
611 :type session: openerpweb.openerpweb.OpenERPSession
613 self.parse_domain(elem, 'domain', session)
614 self.parse_domain(elem, 'filter_domain', session)
615 context_string = elem.get('context', '').strip()
619 openerpweb.ast.literal_eval(context_string))
622 openerpweb.nonliterals.Context(
623 session, context_string))
625 class FormView(View):
626 _cp_path = "/base/formview"
628 @openerpweb.jsonrequest
629 def load(self, req, model, view_id, toolbar=False):
630 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
631 return {'fields_view': fields_view}
633 class ListView(View):
634 _cp_path = "/base/listview"
636 @openerpweb.jsonrequest
637 def load(self, req, model, view_id, toolbar=False):
638 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
639 return {'fields_view': fields_view}
641 def process_colors(self, view, row, context):
642 colors = view['arch']['attrs'].get('colors')
649 for pair in colors.split(';')
650 if eval(pair.split(':')[1], dict(context, **row))
655 elif len(color) == 1:
659 class SearchView(View):
660 _cp_path = "/base/searchview"
662 @openerpweb.jsonrequest
663 def load(self, req, model, view_id):
664 fields_view = self.fields_view_get(req, model, view_id, 'search')
665 return {'fields_view': fields_view}
667 @openerpweb.jsonrequest
668 def fields_get(self, req, model):
669 Model = req.session.model(model)
670 fields = Model.fields_get(False, req.session.eval_context(req.context))
671 return {'fields': fields}
673 class Binary(openerpweb.Controller):
674 _cp_path = "/base/binary"
676 @openerpweb.httprequest
677 def image(self, request, session_id, model, id, field, **kw):
678 cherrypy.response.headers['Content-Type'] = 'image/png'
679 Model = request.session.model(model)
680 context = request.session.eval_context(request.context)
683 res = Model.default_get([field], context).get(field, '')
685 res = Model.read([int(id)], [field], context)[0].get(field, '')
686 return base64.decodestring(res)
687 except: # TODO: what's the exception here?
688 return self.placeholder()
689 def placeholder(self):
690 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
692 @openerpweb.httprequest
693 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
694 Model = request.session.model(model)
695 context = request.session.eval_context(request.context)
696 res = Model.read([int(id)], [field, fieldname], context)[0]
697 filecontent = res.get(field, '')
699 raise cherrypy.NotFound
701 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
702 filename = '%s_%s' % (model.replace('.', '_'), id)
704 filename = res.get(fieldname, '') or filename
705 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
706 return base64.decodestring(filecontent)
708 @openerpweb.httprequest
709 def upload(self, request, session_id, callback, ufile=None):
710 cherrypy.response.timeout = 500
712 for key, val in cherrypy.request.headers.iteritems():
713 headers[key.lower()] = val
714 size = int(headers.get('content-length', 0))
715 # TODO: might be useful to have a configuration flag for max-length file uploads
717 out = """<script language="javascript" type="text/javascript">
718 var win = window.top.window,
720 if (typeof(callback) === 'function') {
721 callback.apply(this, %s);
723 win.jQuery('#oe_notification', win.document).notify('create', {
724 title: "Ajax File Upload",
725 text: "Could not find callback"
729 data = ufile.file.read()
730 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
732 args = [False, e.message]
733 return out % (simplejson.dumps(callback), simplejson.dumps(args))
735 @openerpweb.httprequest
736 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
737 cherrypy.response.timeout = 500
738 context = request.session.eval_context(request.context)
739 Model = request.session.model('ir.attachment')
741 out = """<script language="javascript" type="text/javascript">
742 var win = window.top.window,
744 if (typeof(callback) === 'function') {
745 callback.call(this, %s);
748 attachment_id = Model.create({
749 'name': ufile.filename,
750 'datas': base64.encodestring(ufile.file.read()),
755 'filename': ufile.filename,
759 args = { 'error': e.message }
760 return out % (simplejson.dumps(callback), simplejson.dumps(args))
762 class Action(openerpweb.Controller):
763 _cp_path = "/base/action"
765 @openerpweb.jsonrequest
766 def load(self, req, action_id):
767 Actions = req.session.model('ir.actions.actions')
769 context = req.session.eval_context(req.context)
770 action_type = Actions.read([action_id], ['type'], context)
772 action = req.session.model(action_type[0]['type']).read([action_id], False,
775 value = clean_action(action[0], req.session)
776 return {'result': value}