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 context['bin_size'] = False # Possible upstream bug. Antony says not to loose time on this.
247 Values = req.session.model('ir.values')
248 actions = Values.get(key, key2, models, meta, context)
250 for _, _, action in actions:
251 clean_action(action, req.session)
255 def clean_action(action, session):
256 # values come from the server, we can just eval them
257 if isinstance(action['context'], basestring):
258 action['context'] = eval(
260 session.evaluation_context()) or {}
262 if isinstance(action['domain'], basestring):
263 action['domain'] = eval(
265 session.evaluation_context(
266 action['context'])) or []
267 if not action.has_key('flags'):
268 # Set empty flags dictionary for web client.
269 action['flags'] = dict()
270 return fix_view_modes(action)
272 def fix_view_modes(action):
273 """ For historical reasons, OpenERP has weird dealings in relation to
274 view_mode and the view_type attribute (on window actions):
276 * one of the view modes is ``tree``, which stands for both list views
278 * the choice is made by checking ``view_type``, which is either
279 ``form`` for a list view or ``tree`` for an actual tree view
281 This methods simply folds the view_type into view_mode by adding a
282 new view mode ``list`` which is the result of the ``tree`` view_mode
283 in conjunction with the ``form`` view_type.
285 TODO: this should go into the doc, some kind of "peculiarities" section
287 :param dict action: an action descriptor
288 :returns: nothing, the action is modified in place
290 if action.pop('view_type') != 'form':
293 action['view_mode'] = ','.join(
294 mode if mode != 'tree' else 'list'
295 for mode in action['view_mode'].split(','))
297 [id, mode if mode != 'tree' else 'list']
298 for id, mode in action['views']
302 class Menu(openerpweb.Controller):
303 _cp_path = "/base/menu"
305 @openerpweb.jsonrequest
307 return {'data': self.do_load(req)}
309 def do_load(self, req):
310 """ Loads all menu items (all applications and their sub-menus).
312 :param req: A request object, with an OpenERP session attribute
313 :type req: < session -> OpenERPSession >
314 :return: the menu root
315 :rtype: dict('children': menu_nodes)
317 Menus = req.session.model('ir.ui.menu')
318 # menus are loaded fully unlike a regular tree view, cause there are
319 # less than 512 items
320 context = req.session.eval_context(req.context)
321 menu_ids = Menus.search([], 0, False, False, context)
322 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
323 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
324 menu_items.append(menu_root)
326 # make a tree using parent_id
327 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
328 for menu_item in menu_items:
329 if menu_item['parent_id']:
330 parent = menu_item['parent_id'][0]
333 if parent in menu_items_map:
334 menu_items_map[parent].setdefault(
335 'children', []).append(menu_item)
337 # sort by sequence a tree using parent_id
338 for menu_item in menu_items:
339 menu_item.setdefault('children', []).sort(
340 key=lambda x:x["sequence"])
344 @openerpweb.jsonrequest
345 def action(self, req, menu_id):
346 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
347 [('ir.ui.menu', menu_id)], False,
348 req.session.eval_context(req.context))
349 return {"action": actions}
351 class DataSet(openerpweb.Controller):
352 _cp_path = "/base/dataset"
354 @openerpweb.jsonrequest
355 def fields(self, req, model):
356 return {'fields': req.session.model(model).fields_get(False,
357 req.session.eval_context(req.context))}
359 @openerpweb.jsonrequest
360 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
361 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
362 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
363 """ Performs a search() followed by a read() (if needed) using the
364 provided search criteria
366 :param request: a JSON-RPC request object
367 :type request: openerpweb.JsonRequest
368 :param str model: the name of the model to search on
369 :param fields: a list of the fields to return in the result records
371 :param int offset: from which index should the results start being returned
372 :param int limit: the maximum number of records to return
373 :param list domain: the search domain for the query
374 :param list sort: sorting directives
375 :returns: a list of result records
378 Model = request.session.model(model)
379 context, domain = eval_context_and_domain(request.session, request.context, domain)
381 ids = Model.search(domain, offset or 0, limit or False,
382 sort or False, context)
384 if fields and fields == ['id']:
385 # shortcut read if we only want the ids
386 return map(lambda id: {'id': id}, ids)
388 reads = Model.read(ids, fields or False, context)
389 reads.sort(key=lambda obj: ids.index(obj['id']))
392 @openerpweb.jsonrequest
393 def get(self, request, model, ids, fields=False):
394 return self.do_get(request, model, ids, fields)
395 def do_get(self, request, model, ids, fields=False):
396 """ Fetches and returns the records of the model ``model`` whose ids
399 The results are in the same order as the inputs, but elements may be
400 missing (if there is no record left for the id)
402 :param request: the JSON-RPC2 request object
403 :type request: openerpweb.JsonRequest
404 :param model: the model to read from
406 :param ids: a list of identifiers
408 :param fields: a list of fields to fetch, ``False`` or empty to fetch
409 all fields in the model
410 :type fields: list | False
411 :returns: a list of records, in the same order as the list of ids
414 Model = request.session.model(model)
415 records = Model.read(ids, fields, request.session.eval_context(request.context))
417 record_map = dict((record['id'], record) for record in records)
419 return [record_map[id] for id in ids if record_map.get(id)]
421 @openerpweb.jsonrequest
422 def load(self, req, model, id, fields):
423 m = req.session.model(model)
425 r = m.read([id], False, req.session.eval_context(req.context))
428 return {'value': value}
430 @openerpweb.jsonrequest
431 def create(self, req, model, data):
432 m = req.session.model(model)
433 r = m.create(data, req.session.eval_context(req.context))
436 @openerpweb.jsonrequest
437 def save(self, req, model, id, data):
438 m = req.session.model(model)
439 r = m.write([id], data, req.session.eval_context(req.context))
442 @openerpweb.jsonrequest
443 def unlink(self, request, model, ids=[]):
444 Model = request.session.model(model)
445 return Model.unlink(ids, request.session.eval_context(request.context))
447 @openerpweb.jsonrequest
448 def call(self, req, model, method, args, domain_id=None, context_id=None):
449 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
450 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
451 c, d = eval_context_and_domain(req.session, context, domain);
452 if(domain_id and len(args) - 1 >= domain_id):
454 if(context_id and len(args) - 1 >= context_id):
457 m = req.session.model(model)
458 r = getattr(m, method)(*args)
461 @openerpweb.jsonrequest
462 def exec_workflow(self, req, model, id, signal):
463 r = req.session.exec_workflow(model, id, signal)
466 @openerpweb.jsonrequest
467 def default_get(self, req, model, fields):
468 m = req.session.model(model)
469 r = m.default_get(fields, req.session.eval_context(req.context))
472 class DataGroup(openerpweb.Controller):
473 _cp_path = "/base/group"
474 @openerpweb.jsonrequest
475 def read(self, request, model, group_by_fields, domain=None):
476 Model = request.session.model(model)
477 context, domain = eval_context_and_domain(request.session, request.context, domain)
479 return Model.read_group(
480 domain or [], False, group_by_fields, 0, False,
481 dict(context, group_by=group_by_fields))
483 class View(openerpweb.Controller):
484 _cp_path = "/base/view"
486 def fields_view_get(self, request, model, view_id, view_type,
487 transform=True, toolbar=False, submenu=False):
488 Model = request.session.model(model)
489 context = request.session.eval_context(request.context)
490 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
491 # todo fme?: check that we should pass the evaluated context here
492 self.process_view(request.session, fvg, context, transform)
495 def process_view(self, session, fvg, context, transform):
497 evaluation_context = session.evaluation_context(context or {})
498 xml = self.transform_view(fvg['arch'], session, evaluation_context)
500 xml = ElementTree.fromstring(fvg['arch'])
501 fvg['arch'] = Xml2Json.convert_element(xml)
502 for field in fvg['fields'].values():
503 if field.has_key('views') and field['views']:
504 for view in field["views"].values():
505 self.process_view(session, view, None, transform)
507 @openerpweb.jsonrequest
508 def add_custom(self, request, view_id, arch):
509 CustomView = request.session.model('ir.ui.view.custom')
511 'user_id': request.session._uid,
514 }, request.session.eval_context(request.context))
515 return {'result': True}
517 @openerpweb.jsonrequest
518 def undo_custom(self, request, view_id, reset=False):
519 CustomView = request.session.model('ir.ui.view.custom')
520 context = request.session.eval_context(request.context)
521 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
522 0, False, False, context)
525 CustomView.unlink(vcustom, context)
527 CustomView.unlink([vcustom[0]], context)
528 return {'result': True}
529 return {'result': False}
531 def normalize_attrs(self, elem, context):
532 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
533 the client only has to deal with @attrs.
535 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
538 :param elem: the current view node (Python object)
539 :type elem: xml.etree.ElementTree.Element
540 :param dict context: evaluation context
542 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
543 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
544 if 'states' in elem.attrib:
545 attrs.setdefault('invisible', [])\
546 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
548 elem.set('attrs', simplejson.dumps(attrs))
549 for a in ['invisible', 'readonly', 'required']:
551 # In the XML we trust
552 avalue = bool(eval(elem.get(a, 'False'),
553 {'context': context or {}}))
558 if a == 'invisible' and 'attrs' in elem.attrib:
559 del elem.attrib['attrs']
561 def transform_view(self, view_string, session, context=None):
562 # transform nodes on the fly via iterparse, instead of
563 # doing it statically on the parsing result
564 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
566 for event, elem in parser:
570 self.normalize_attrs(elem, context)
571 self.parse_domains_and_contexts(elem, session)
574 def parse_domain(self, elem, attr_name, session):
575 """ Parses an attribute of the provided name as a domain, transforms it
576 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
578 :param elem: the node being parsed
579 :type param: xml.etree.ElementTree.Element
580 :param str attr_name: the name of the attribute which should be parsed
581 :param session: Current OpenERP session
582 :type session: openerpweb.openerpweb.OpenERPSession
584 domain = elem.get(attr_name, '').strip()
589 openerpweb.ast.literal_eval(
594 openerpweb.nonliterals.Domain(session, domain))
596 def parse_domains_and_contexts(self, elem, session):
597 """ Converts domains and contexts from the view into Python objects,
598 either literals if they can be parsed by literal_eval or a special
599 placeholder object if the domain or context refers to free variables.
601 :param elem: the current node being parsed
602 :type param: xml.etree.ElementTree.Element
603 :param session: OpenERP session object, used to store and retrieve
605 :type session: openerpweb.openerpweb.OpenERPSession
607 self.parse_domain(elem, 'domain', session)
608 self.parse_domain(elem, 'filter_domain', session)
609 context_string = elem.get('context', '').strip()
613 openerpweb.ast.literal_eval(context_string))
616 openerpweb.nonliterals.Context(
617 session, context_string))
619 class FormView(View):
620 _cp_path = "/base/formview"
622 @openerpweb.jsonrequest
623 def load(self, req, model, view_id, toolbar=False):
624 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
625 return {'fields_view': fields_view}
627 class ListView(View):
628 _cp_path = "/base/listview"
630 @openerpweb.jsonrequest
631 def load(self, req, model, view_id, toolbar=False):
632 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
633 return {'fields_view': fields_view}
635 def process_colors(self, view, row, context):
636 colors = view['arch']['attrs'].get('colors')
643 for pair in colors.split(';')
644 if eval(pair.split(':')[1], dict(context, **row))
649 elif len(color) == 1:
653 class SearchView(View):
654 _cp_path = "/base/searchview"
656 @openerpweb.jsonrequest
657 def load(self, req, model, view_id):
658 fields_view = self.fields_view_get(req, model, view_id, 'search')
659 return {'fields_view': fields_view}
661 @openerpweb.jsonrequest
662 def fields_get(self, req, model):
663 Model = req.session.model(model)
664 fields = Model.fields_get(False, req.session.eval_context(req.context))
665 return {'fields': fields}
667 class Binary(openerpweb.Controller):
668 _cp_path = "/base/binary"
670 @openerpweb.httprequest
671 def image(self, request, session_id, model, id, field, **kw):
672 cherrypy.response.headers['Content-Type'] = 'image/png'
673 Model = request.session.model(model)
674 context = request.session.eval_context(request.context)
677 res = Model.default_get([field], context).get(field, '')
679 res = Model.read([int(id)], [field], context)[0].get(field, '')
680 return base64.decodestring(res)
681 except: # TODO: what's the exception here?
682 return self.placeholder()
683 def placeholder(self):
684 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
686 @openerpweb.httprequest
687 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
688 Model = request.session.model(model)
689 context = request.session.eval_context(request.context)
690 res = Model.read([int(id)], [field, fieldname], context)[0]
691 filecontent = res.get(field, '')
693 raise cherrypy.NotFound
695 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
696 filename = '%s_%s' % (model.replace('.', '_'), id)
698 filename = res.get(fieldname, '') or filename
699 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
700 return base64.decodestring(filecontent)
702 @openerpweb.httprequest
703 def upload(self, request, session_id, callback, ufile=None):
704 cherrypy.response.timeout = 500
706 for key, val in cherrypy.request.headers.iteritems():
707 headers[key.lower()] = val
708 size = int(headers.get('content-length', 0))
709 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
711 out = """<script language="javascript" type="text/javascript">
712 var win = window.top.window,
714 if (typeof(callback) === 'function') {
715 callback.apply(this, %s);
717 win.jQuery('#oe_notification', win.document).notify('create', {
718 title: "Ajax File Upload",
719 text: "Could not find callback"
723 data = ufile.file.read()
724 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
726 args = [False, e.message]
727 return out % (simplejson.dumps(callback), simplejson.dumps(args))
729 @openerpweb.httprequest
730 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
731 cherrypy.response.timeout = 500
732 context = request.session.eval_context(request.context)
733 Model = request.session.model('ir.attachment')
735 out = """<script language="javascript" type="text/javascript">
736 var win = window.top.window,
738 if (typeof(callback) === 'function') {
739 callback.call(this, %s);
742 attachment_id = Model.create({
743 'name': ufile.filename,
744 'datas': base64.encodestring(ufile.file.read()),
749 'filename': ufile.filename,
753 args = { 'error': e.message }
754 return out % (simplejson.dumps(callback), simplejson.dumps(args))
756 class Action(openerpweb.Controller):
757 _cp_path = "/base/action"
759 @openerpweb.jsonrequest
760 def load(self, req, action_id):
761 Actions = req.session.model('ir.actions.actions')
763 context = req.session.eval_context(req.context)
764 context["bin_size"] = False
765 action_type = Actions.read([action_id], ['type'], context)
767 action = req.session.model(action_type[0]['type']).read([action_id], False,
770 value = clean_action(action[0], req.session)
771 return {'result': value}