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 = req.session.eval_context(openerpweb.nonliterals.CompoundContext(*contexts))
175 domain = req.session.eval_domain(openerpweb.nonliterals.CompoundDomain(*(domains or [])), context)
177 group_by_sequence = []
178 for candidate in (group_by_seq or []):
179 ctx = req.session.eval_context(candidate, context)
180 group_by = ctx.get('group_by')
183 elif isinstance(group_by, basestring):
184 group_by_sequence.append(group_by)
186 group_by_sequence.extend(group_by)
191 'group_by': group_by_sequence
194 @openerpweb.jsonrequest
195 def save_session_action(self, req, the_action):
197 This method store an action object in the session object and returns an integer
198 identifying that action. The method get_session_action() can be used to get
201 :param the_action: The action to save in the session.
202 :type the_action: anything
203 :return: A key identifying the saved action.
206 saved_actions = cherrypy.session.get('saved_actions')
207 if not saved_actions:
208 saved_actions = {"next":0, "actions":{}}
209 cherrypy.session['saved_actions'] = saved_actions
210 # we don't allow more than 10 stored actions
211 if len(saved_actions["actions"]) >= 10:
212 del saved_actions["actions"][min(saved_actions["actions"].keys())]
213 key = saved_actions["next"]
214 saved_actions["actions"][key] = the_action
215 saved_actions["next"] = key + 1
218 @openerpweb.jsonrequest
219 def get_session_action(self, req, key):
221 Gets back a previously saved action. This method can return None if the action
222 was saved since too much time (this case should be handled in a smart way).
224 :param key: The key given by save_session_action()
226 :return: The saved action or None.
229 saved_actions = cherrypy.session.get('saved_actions')
230 if not saved_actions:
232 return saved_actions["actions"].get(key)
234 def eval_context_and_domain(session, context, domain=None):
235 e_context = session.eval_context(context)
236 e_domain = session.eval_domain(domain or [], e_context)
238 return (e_context, e_domain)
240 def load_actions_from_ir_values(req, key, key2, models, meta, context):
241 context['bin_size'] = False # Possible upstream bug. Antony says not to loose time on this.
242 Values = req.session.model('ir.values')
243 actions = Values.get(key, key2, models, meta, context)
245 for _, _, action in actions:
246 clean_action(action, req.session)
250 def clean_action(action, session):
251 # values come from the server, we can just eval them
252 if isinstance(action['context'], basestring):
253 action['context'] = eval(
255 session.evaluation_context()) or {}
257 if isinstance(action['domain'], basestring):
258 action['domain'] = eval(
260 session.evaluation_context(
261 action['context'])) or []
262 if not action.has_key('flags'):
263 # Set empty flags dictionary for web client.
264 action['flags'] = dict()
265 return fix_view_modes(action)
267 def fix_view_modes(action):
268 """ For historical reasons, OpenERP has weird dealings in relation to
269 view_mode and the view_type attribute (on window actions):
271 * one of the view modes is ``tree``, which stands for both list views
273 * the choice is made by checking ``view_type``, which is either
274 ``form`` for a list view or ``tree`` for an actual tree view
276 This methods simply folds the view_type into view_mode by adding a
277 new view mode ``list`` which is the result of the ``tree`` view_mode
278 in conjunction with the ``form`` view_type.
280 TODO: this should go into the doc, some kind of "peculiarities" section
282 :param dict action: an action descriptor
283 :returns: nothing, the action is modified in place
285 if action.pop('view_type') != 'form':
288 action['view_mode'] = ','.join(
289 mode if mode != 'tree' else 'list'
290 for mode in action['view_mode'].split(','))
292 [id, mode if mode != 'tree' else 'list']
293 for id, mode in action['views']
297 class Menu(openerpweb.Controller):
298 _cp_path = "/base/menu"
300 @openerpweb.jsonrequest
302 return {'data': self.do_load(req)}
304 def do_load(self, req):
305 """ Loads all menu items (all applications and their sub-menus).
307 :param req: A request object, with an OpenERP session attribute
308 :type req: < session -> OpenERPSession >
309 :return: the menu root
310 :rtype: dict('children': menu_nodes)
312 Menus = req.session.model('ir.ui.menu')
313 # menus are loaded fully unlike a regular tree view, cause there are
314 # less than 512 items
315 context = req.session.eval_context(req.context)
316 menu_ids = Menus.search([], 0, False, False, context)
317 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
318 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
319 menu_items.append(menu_root)
321 # make a tree using parent_id
322 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
323 for menu_item in menu_items:
324 if menu_item['parent_id']:
325 parent = menu_item['parent_id'][0]
328 if parent in menu_items_map:
329 menu_items_map[parent].setdefault(
330 'children', []).append(menu_item)
332 # sort by sequence a tree using parent_id
333 for menu_item in menu_items:
334 menu_item.setdefault('children', []).sort(
335 key=lambda x:x["sequence"])
339 @openerpweb.jsonrequest
340 def action(self, req, menu_id):
341 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
342 [('ir.ui.menu', menu_id)], False,
343 req.session.eval_context(req.context))
344 return {"action": actions}
346 class DataSet(openerpweb.Controller):
347 _cp_path = "/base/dataset"
349 @openerpweb.jsonrequest
350 def fields(self, req, model):
351 return {'fields': req.session.model(model).fields_get(False,
352 req.session.eval_context(req.context))}
354 @openerpweb.jsonrequest
355 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
356 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
357 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
358 """ Performs a search() followed by a read() (if needed) using the
359 provided search criteria
361 :param request: a JSON-RPC request object
362 :type request: openerpweb.JsonRequest
363 :param str model: the name of the model to search on
364 :param fields: a list of the fields to return in the result records
366 :param int offset: from which index should the results start being returned
367 :param int limit: the maximum number of records to return
368 :param list domain: the search domain for the query
369 :param list sort: sorting directives
370 :returns: a list of result records
373 Model = request.session.model(model)
374 context, domain = eval_context_and_domain(request.session, request.context, domain)
376 ids = Model.search(domain, offset or 0, limit or False,
377 sort or False, context)
379 if fields and fields == ['id']:
380 # shortcut read if we only want the ids
381 return map(lambda id: {'id': id}, ids)
383 reads = Model.read(ids, fields or False, context)
384 reads.sort(key=lambda obj: ids.index(obj['id']))
387 @openerpweb.jsonrequest
388 def read(self, request, model, ids, fields=False):
389 return self.do_search_read(request, model, ids, fields,
390 request.session.eval_context(request.context))
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 action_type = Actions.read([action_id], ['type'], context)
766 action = req.session.model(action_type[0]['type']).read([action_id], False,
767 #TODO fme: check why does not work with context
771 value = clean_action(action[0], req.session)
772 return {'result': value}