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 e_domain = session.eval_domain(domain or [], e_context)
239 return (e_context, e_domain)
241 def load_actions_from_ir_values(req, key, key2, models, meta, context):
242 context['bin_size'] = False # Possible upstream bug. Antony says not to loose time on this.
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['context'], basestring):
254 action['context'] = eval(
256 session.evaluation_context()) or {}
258 if isinstance(action['domain'], 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 action['view_mode'] = ','.join(
290 mode if mode != 'tree' else 'list'
291 for mode in action['view_mode'].split(','))
293 [id, mode if mode != 'tree' else 'list']
294 for id, mode in action['views']
298 class Menu(openerpweb.Controller):
299 _cp_path = "/base/menu"
301 @openerpweb.jsonrequest
303 return {'data': self.do_load(req)}
305 def do_load(self, req):
306 """ Loads all menu items (all applications and their sub-menus).
308 :param req: A request object, with an OpenERP session attribute
309 :type req: < session -> OpenERPSession >
310 :return: the menu root
311 :rtype: dict('children': menu_nodes)
313 Menus = req.session.model('ir.ui.menu')
314 # menus are loaded fully unlike a regular tree view, cause there are
315 # less than 512 items
316 context = req.session.eval_context(req.context)
317 menu_ids = Menus.search([], 0, False, False, context)
318 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
319 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
320 menu_items.append(menu_root)
322 # make a tree using parent_id
323 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
324 for menu_item in menu_items:
325 if menu_item['parent_id']:
326 parent = menu_item['parent_id'][0]
329 if parent in menu_items_map:
330 menu_items_map[parent].setdefault(
331 'children', []).append(menu_item)
333 # sort by sequence a tree using parent_id
334 for menu_item in menu_items:
335 menu_item.setdefault('children', []).sort(
336 key=lambda x:x["sequence"])
340 @openerpweb.jsonrequest
341 def action(self, req, menu_id):
342 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
343 [('ir.ui.menu', menu_id)], False,
344 req.session.eval_context(req.context))
345 return {"action": actions}
347 class DataSet(openerpweb.Controller):
348 _cp_path = "/base/dataset"
350 @openerpweb.jsonrequest
351 def fields(self, req, model):
352 return {'fields': req.session.model(model).fields_get(False,
353 req.session.eval_context(req.context))}
355 @openerpweb.jsonrequest
356 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
357 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
358 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
359 """ Performs a search() followed by a read() (if needed) using the
360 provided search criteria
362 :param request: a JSON-RPC request object
363 :type request: openerpweb.JsonRequest
364 :param str model: the name of the model to search on
365 :param fields: a list of the fields to return in the result records
367 :param int offset: from which index should the results start being returned
368 :param int limit: the maximum number of records to return
369 :param list domain: the search domain for the query
370 :param list sort: sorting directives
371 :returns: a list of result records
374 Model = request.session.model(model)
375 context, domain = eval_context_and_domain(request.session, request.context, domain)
377 ids = Model.search(domain, offset or 0, limit or False,
378 sort or False, context)
380 if fields and fields == ['id']:
381 # shortcut read if we only want the ids
382 return map(lambda id: {'id': id}, ids)
384 reads = Model.read(ids, fields or False, context)
385 reads.sort(key=lambda obj: ids.index(obj['id']))
388 @openerpweb.jsonrequest
389 def get(self, request, model, ids, fields=False):
390 return self.do_get(request, model, ids, fields)
391 def do_get(self, request, model, ids, fields=False):
392 """ Fetches and returns the records of the model ``model`` whose ids
395 The results are in the same order as the inputs, but elements may be
396 missing (if there is no record left for the id)
398 :param request: the JSON-RPC2 request object
399 :type request: openerpweb.JsonRequest
400 :param model: the model to read from
402 :param ids: a list of identifiers
404 :param fields: a list of fields to fetch, ``False`` or empty to fetch
405 all fields in the model
406 :type fields: list | False
407 :returns: a list of records, in the same order as the list of ids
410 Model = request.session.model(model)
411 records = Model.read(ids, fields, request.session.eval_context(request.context))
413 record_map = dict((record['id'], record) for record in records)
415 return [record_map[id] for id in ids if record_map.get(id)]
417 @openerpweb.jsonrequest
418 def load(self, req, model, id, fields):
419 m = req.session.model(model)
421 r = m.read([id], False, req.session.eval_context(req.context))
424 return {'value': value}
426 @openerpweb.jsonrequest
427 def create(self, req, model, data):
428 m = req.session.model(model)
429 r = m.create(data, req.session.eval_context(req.context))
432 @openerpweb.jsonrequest
433 def save(self, req, model, id, data):
434 m = req.session.model(model)
435 r = m.write([id], data, req.session.eval_context(req.context))
438 @openerpweb.jsonrequest
439 def unlink(self, request, model, ids=[]):
440 Model = request.session.model(model)
441 return Model.unlink(ids, request.session.eval_context(request.context))
443 @openerpweb.jsonrequest
444 def call(self, req, model, method, args, domain_id=None, context_id=None):
445 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
446 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
447 c, d = eval_context_and_domain(req.session, context, domain);
448 if(domain_id and len(args) - 1 >= domain_id):
450 if(context_id and len(args) - 1 >= context_id):
453 m = req.session.model(model)
454 r = getattr(m, method)(*args)
457 @openerpweb.jsonrequest
458 def exec_workflow(self, req, model, id, signal):
459 r = req.session.exec_workflow(model, id, signal)
462 @openerpweb.jsonrequest
463 def default_get(self, req, model, fields):
464 m = req.session.model(model)
465 r = m.default_get(fields, req.session.eval_context(req.context))
468 class DataGroup(openerpweb.Controller):
469 _cp_path = "/base/group"
470 @openerpweb.jsonrequest
471 def read(self, request, model, group_by_fields, domain=None):
472 Model = request.session.model(model)
473 context, domain = eval_context_and_domain(request.session, request.context, domain)
475 return Model.read_group(
476 domain or [], False, group_by_fields, 0, False,
477 dict(context, group_by=group_by_fields))
479 class View(openerpweb.Controller):
480 _cp_path = "/base/view"
482 def fields_view_get(self, request, model, view_id, view_type,
483 transform=True, toolbar=False, submenu=False):
484 Model = request.session.model(model)
485 context = request.session.eval_context(request.context)
486 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
487 # todo fme?: check that we should pass the evaluated context here
488 self.process_view(request.session, fvg, context, transform)
491 def process_view(self, session, fvg, context, transform):
493 evaluation_context = session.evaluation_context(context or {})
494 xml = self.transform_view(fvg['arch'], session, evaluation_context)
496 xml = ElementTree.fromstring(fvg['arch'])
497 fvg['arch'] = Xml2Json.convert_element(xml)
498 for field in fvg['fields'].values():
499 if field.has_key('views') and field['views']:
500 for view in field["views"].values():
501 self.process_view(session, view, None, transform)
503 @openerpweb.jsonrequest
504 def add_custom(self, request, view_id, arch):
505 CustomView = request.session.model('ir.ui.view.custom')
507 'user_id': request.session._uid,
510 }, request.session.eval_context(request.context))
511 return {'result': True}
513 @openerpweb.jsonrequest
514 def undo_custom(self, request, view_id, reset=False):
515 CustomView = request.session.model('ir.ui.view.custom')
516 context = request.session.eval_context(request.context)
517 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
518 0, False, False, context)
521 CustomView.unlink(vcustom, context)
523 CustomView.unlink([vcustom[0]], context)
524 return {'result': True}
525 return {'result': False}
527 def normalize_attrs(self, elem, context):
528 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
529 the client only has to deal with @attrs.
531 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
534 :param elem: the current view node (Python object)
535 :type elem: xml.etree.ElementTree.Element
536 :param dict context: evaluation context
538 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
539 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
540 if 'states' in elem.attrib:
541 attrs.setdefault('invisible', [])\
542 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
544 elem.set('attrs', simplejson.dumps(attrs))
545 for a in ['invisible', 'readonly', 'required']:
547 # In the XML we trust
548 avalue = bool(eval(elem.get(a, 'False'),
549 {'context': context or {}}))
554 if a == 'invisible' and 'attrs' in elem.attrib:
555 del elem.attrib['attrs']
557 def transform_view(self, view_string, session, context=None):
558 # transform nodes on the fly via iterparse, instead of
559 # doing it statically on the parsing result
560 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
562 for event, elem in parser:
566 self.normalize_attrs(elem, context)
567 self.parse_domains_and_contexts(elem, session)
570 def parse_domain(self, elem, attr_name, session):
571 """ Parses an attribute of the provided name as a domain, transforms it
572 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
574 :param elem: the node being parsed
575 :type param: xml.etree.ElementTree.Element
576 :param str attr_name: the name of the attribute which should be parsed
577 :param session: Current OpenERP session
578 :type session: openerpweb.openerpweb.OpenERPSession
580 domain = elem.get(attr_name, '').strip()
585 openerpweb.ast.literal_eval(
590 openerpweb.nonliterals.Domain(session, domain))
592 def parse_domains_and_contexts(self, elem, session):
593 """ Converts domains and contexts from the view into Python objects,
594 either literals if they can be parsed by literal_eval or a special
595 placeholder object if the domain or context refers to free variables.
597 :param elem: the current node being parsed
598 :type param: xml.etree.ElementTree.Element
599 :param session: OpenERP session object, used to store and retrieve
601 :type session: openerpweb.openerpweb.OpenERPSession
603 self.parse_domain(elem, 'domain', session)
604 self.parse_domain(elem, 'filter_domain', session)
605 context_string = elem.get('context', '').strip()
609 openerpweb.ast.literal_eval(context_string))
612 openerpweb.nonliterals.Context(
613 session, context_string))
615 class FormView(View):
616 _cp_path = "/base/formview"
618 @openerpweb.jsonrequest
619 def load(self, req, model, view_id, toolbar=False):
620 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
621 return {'fields_view': fields_view}
623 class ListView(View):
624 _cp_path = "/base/listview"
626 @openerpweb.jsonrequest
627 def load(self, req, model, view_id, toolbar=False):
628 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
629 return {'fields_view': fields_view}
631 def process_colors(self, view, row, context):
632 colors = view['arch']['attrs'].get('colors')
639 for pair in colors.split(';')
640 if eval(pair.split(':')[1], dict(context, **row))
645 elif len(color) == 1:
649 class SearchView(View):
650 _cp_path = "/base/searchview"
652 @openerpweb.jsonrequest
653 def load(self, req, model, view_id):
654 fields_view = self.fields_view_get(req, model, view_id, 'search')
655 return {'fields_view': fields_view}
657 @openerpweb.jsonrequest
658 def fields_get(self, req, model):
659 Model = req.session.model(model)
660 fields = Model.fields_get(False, req.session.eval_context(req.context))
661 return {'fields': fields}
663 class Binary(openerpweb.Controller):
664 _cp_path = "/base/binary"
666 @openerpweb.httprequest
667 def image(self, request, session_id, model, id, field, **kw):
668 cherrypy.response.headers['Content-Type'] = 'image/png'
669 Model = request.session.model(model)
670 context = request.session.eval_context(request.context)
673 res = Model.default_get([field], context).get(field, '')
675 res = Model.read([int(id)], [field], context)[0].get(field, '')
676 return base64.decodestring(res)
677 except: # TODO: what's the exception here?
678 return self.placeholder()
679 def placeholder(self):
680 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
682 @openerpweb.httprequest
683 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
684 Model = request.session.model(model)
685 context = request.session.eval_context(request.context)
686 res = Model.read([int(id)], [field, fieldname], context)[0]
687 filecontent = res.get(field, '')
689 raise cherrypy.NotFound
691 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
692 filename = '%s_%s' % (model.replace('.', '_'), id)
694 filename = res.get(fieldname, '') or filename
695 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
696 return base64.decodestring(filecontent)
698 @openerpweb.httprequest
699 def upload(self, request, session_id, callback, ufile=None):
700 cherrypy.response.timeout = 500
702 for key, val in cherrypy.request.headers.iteritems():
703 headers[key.lower()] = val
704 size = int(headers.get('content-length', 0))
705 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
707 out = """<script language="javascript" type="text/javascript">
708 var win = window.top.window,
710 if (typeof(callback) === 'function') {
711 callback.apply(this, %s);
713 win.jQuery('#oe_notification', win.document).notify('create', {
714 title: "Ajax File Upload",
715 text: "Could not find callback"
719 data = ufile.file.read()
720 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
722 args = [False, e.message]
723 return out % (simplejson.dumps(callback), simplejson.dumps(args))
725 @openerpweb.httprequest
726 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
727 cherrypy.response.timeout = 500
728 context = request.session.eval_context(request.context)
729 Model = request.session.model('ir.attachment')
731 out = """<script language="javascript" type="text/javascript">
732 var win = window.top.window,
734 if (typeof(callback) === 'function') {
735 callback.call(this, %s);
738 attachment_id = Model.create({
739 'name': ufile.filename,
740 'datas': base64.encodestring(ufile.file.read()),
745 'filename': ufile.filename,
749 args = { 'error': e.message }
750 return out % (simplejson.dumps(callback), simplejson.dumps(args))
752 class Action(openerpweb.Controller):
753 _cp_path = "/base/action"
755 @openerpweb.jsonrequest
756 def load(self, req, action_id):
757 Actions = req.session.model('ir.actions.actions')
759 context = req.session.eval_context(req.context)
760 context["bin_size"] = False
761 action_type = Actions.read([action_id], ['type'], context)
763 action = req.session.model(action_type[0]['type']).read([action_id], False,
766 value = clean_action(action[0], req.session)
767 return {'result': value}