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 for _, _, action in actions:
247 clean_action(action, req.session)
251 def clean_action(action, session):
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 not action.has_key('flags'):
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 action.has_key('view_mode'):
290 action['view_mode'] = ','.join(
291 mode if mode != 'tree' else 'list'
292 for mode in action['view_mode'].split(','))
293 if action.has_key('views'):
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 m = req.session.model(model)
455 return getattr(m, method)(*args)
457 @openerpweb.jsonrequest
458 def call(self, req, model, method, args, domain_id=None, context_id=None):
459 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
461 @openerpweb.jsonrequest
462 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
463 action = self.call_common(req, model, method, args, domain_id, context_id)
464 if isinstance(action, dict) and action.get('type') != '':
465 clean_action(action, req.session)
466 return {'result': action}
468 @openerpweb.jsonrequest
469 def exec_workflow(self, req, model, id, signal):
470 r = req.session.exec_workflow(model, id, signal)
473 @openerpweb.jsonrequest
474 def default_get(self, req, model, fields):
475 m = req.session.model(model)
476 r = m.default_get(fields, req.session.eval_context(req.context))
479 class DataGroup(openerpweb.Controller):
480 _cp_path = "/base/group"
481 @openerpweb.jsonrequest
482 def read(self, request, model, group_by_fields, domain=None):
483 Model = request.session.model(model)
484 context, domain = eval_context_and_domain(request.session, request.context, domain)
486 return Model.read_group(
487 domain or [], False, group_by_fields, 0, False,
488 dict(context, group_by=group_by_fields))
490 class View(openerpweb.Controller):
491 _cp_path = "/base/view"
493 def fields_view_get(self, request, model, view_id, view_type,
494 transform=True, toolbar=False, submenu=False):
495 Model = request.session.model(model)
496 context = request.session.eval_context(request.context)
497 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
498 # todo fme?: check that we should pass the evaluated context here
499 self.process_view(request.session, fvg, context, transform)
502 def process_view(self, session, fvg, context, transform):
504 evaluation_context = session.evaluation_context(context or {})
505 xml = self.transform_view(fvg['arch'], session, evaluation_context)
507 xml = ElementTree.fromstring(fvg['arch'])
508 fvg['arch'] = Xml2Json.convert_element(xml)
509 for field in fvg['fields'].values():
510 if field.has_key('views') and field['views']:
511 for view in field["views"].values():
512 self.process_view(session, view, None, transform)
514 @openerpweb.jsonrequest
515 def add_custom(self, request, view_id, arch):
516 CustomView = request.session.model('ir.ui.view.custom')
518 'user_id': request.session._uid,
521 }, request.session.eval_context(request.context))
522 return {'result': True}
524 @openerpweb.jsonrequest
525 def undo_custom(self, request, view_id, reset=False):
526 CustomView = request.session.model('ir.ui.view.custom')
527 context = request.session.eval_context(request.context)
528 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
529 0, False, False, context)
532 CustomView.unlink(vcustom, context)
534 CustomView.unlink([vcustom[0]], context)
535 return {'result': True}
536 return {'result': False}
538 def normalize_attrs(self, elem, context):
539 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
540 the client only has to deal with @attrs.
542 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
545 :param elem: the current view node (Python object)
546 :type elem: xml.etree.ElementTree.Element
547 :param dict context: evaluation context
549 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
550 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
551 if 'states' in elem.attrib:
552 attrs.setdefault('invisible', [])\
553 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
555 elem.set('attrs', simplejson.dumps(attrs))
556 for a in ['invisible', 'readonly', 'required']:
558 # In the XML we trust
559 avalue = bool(eval(elem.get(a, 'False'),
560 {'context': context or {}}))
565 if a == 'invisible' and 'attrs' in elem.attrib:
566 del elem.attrib['attrs']
568 def transform_view(self, view_string, session, context=None):
569 # transform nodes on the fly via iterparse, instead of
570 # doing it statically on the parsing result
571 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
573 for event, elem in parser:
577 self.normalize_attrs(elem, context)
578 self.parse_domains_and_contexts(elem, session)
581 def parse_domain(self, elem, attr_name, session):
582 """ Parses an attribute of the provided name as a domain, transforms it
583 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
585 :param elem: the node being parsed
586 :type param: xml.etree.ElementTree.Element
587 :param str attr_name: the name of the attribute which should be parsed
588 :param session: Current OpenERP session
589 :type session: openerpweb.openerpweb.OpenERPSession
591 domain = elem.get(attr_name, '').strip()
596 openerpweb.ast.literal_eval(
601 openerpweb.nonliterals.Domain(session, domain))
603 def parse_domains_and_contexts(self, elem, session):
604 """ Converts domains and contexts from the view into Python objects,
605 either literals if they can be parsed by literal_eval or a special
606 placeholder object if the domain or context refers to free variables.
608 :param elem: the current node being parsed
609 :type param: xml.etree.ElementTree.Element
610 :param session: OpenERP session object, used to store and retrieve
612 :type session: openerpweb.openerpweb.OpenERPSession
614 self.parse_domain(elem, 'domain', session)
615 self.parse_domain(elem, 'filter_domain', session)
616 context_string = elem.get('context', '').strip()
620 openerpweb.ast.literal_eval(context_string))
623 openerpweb.nonliterals.Context(
624 session, context_string))
626 class FormView(View):
627 _cp_path = "/base/formview"
629 @openerpweb.jsonrequest
630 def load(self, req, model, view_id, toolbar=False):
631 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
632 return {'fields_view': fields_view}
634 class ListView(View):
635 _cp_path = "/base/listview"
637 @openerpweb.jsonrequest
638 def load(self, req, model, view_id, toolbar=False):
639 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
640 return {'fields_view': fields_view}
642 def process_colors(self, view, row, context):
643 colors = view['arch']['attrs'].get('colors')
650 for pair in colors.split(';')
651 if eval(pair.split(':')[1], dict(context, **row))
656 elif len(color) == 1:
660 class SearchView(View):
661 _cp_path = "/base/searchview"
663 @openerpweb.jsonrequest
664 def load(self, req, model, view_id):
665 fields_view = self.fields_view_get(req, model, view_id, 'search')
666 return {'fields_view': fields_view}
668 @openerpweb.jsonrequest
669 def fields_get(self, req, model):
670 Model = req.session.model(model)
671 fields = Model.fields_get(False, req.session.eval_context(req.context))
672 return {'fields': fields}
674 class Binary(openerpweb.Controller):
675 _cp_path = "/base/binary"
677 @openerpweb.httprequest
678 def image(self, request, session_id, model, id, field, **kw):
679 cherrypy.response.headers['Content-Type'] = 'image/png'
680 Model = request.session.model(model)
681 context = request.session.eval_context(request.context)
684 res = Model.default_get([field], context).get(field, '')
686 res = Model.read([int(id)], [field], context)[0].get(field, '')
687 return base64.decodestring(res)
688 except: # TODO: what's the exception here?
689 return self.placeholder()
690 def placeholder(self):
691 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
693 @openerpweb.httprequest
694 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
695 Model = request.session.model(model)
696 context = request.session.eval_context(request.context)
697 res = Model.read([int(id)], [field, fieldname], context)[0]
698 filecontent = res.get(field, '')
700 raise cherrypy.NotFound
702 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
703 filename = '%s_%s' % (model.replace('.', '_'), id)
705 filename = res.get(fieldname, '') or filename
706 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
707 return base64.decodestring(filecontent)
709 @openerpweb.httprequest
710 def upload(self, request, session_id, callback, ufile=None):
711 cherrypy.response.timeout = 500
713 for key, val in cherrypy.request.headers.iteritems():
714 headers[key.lower()] = val
715 size = int(headers.get('content-length', 0))
716 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
718 out = """<script language="javascript" type="text/javascript">
719 var win = window.top.window,
721 if (typeof(callback) === 'function') {
722 callback.apply(this, %s);
724 win.jQuery('#oe_notification', win.document).notify('create', {
725 title: "Ajax File Upload",
726 text: "Could not find callback"
730 data = ufile.file.read()
731 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
733 args = [False, e.message]
734 return out % (simplejson.dumps(callback), simplejson.dumps(args))
736 @openerpweb.httprequest
737 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
738 cherrypy.response.timeout = 500
739 context = request.session.eval_context(request.context)
740 Model = request.session.model('ir.attachment')
742 out = """<script language="javascript" type="text/javascript">
743 var win = window.top.window,
745 if (typeof(callback) === 'function') {
746 callback.call(this, %s);
749 attachment_id = Model.create({
750 'name': ufile.filename,
751 'datas': base64.encodestring(ufile.file.read()),
756 'filename': ufile.filename,
760 args = { 'error': e.message }
761 return out % (simplejson.dumps(callback), simplejson.dumps(args))
763 class Action(openerpweb.Controller):
764 _cp_path = "/base/action"
766 @openerpweb.jsonrequest
767 def load(self, req, action_id):
768 Actions = req.session.model('ir.actions.actions')
770 context = req.session.eval_context(req.context)
771 action_type = Actions.read([action_id], ['type'], context)
773 action = req.session.model(action_type[0]['type']).read([action_id], False,
776 value = clean_action(action[0], req.session)
777 return {'result': value}