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 get(self, request, model, ids, fields=False):
389 return self.do_get(request, model, ids, fields)
390 def do_get(self, request, model, ids, fields=False):
391 """ Fetches and returns the records of the model ``model`` whose ids
394 The results are in the same order as the inputs, but elements may be
395 missing (if there is no record left for the id)
397 :param request: the JSON-RPC2 request object
398 :type request: openerpweb.JsonRequest
399 :param model: the model to read from
401 :param ids: a list of identifiers
403 :param fields: a list of fields to fetch, ``False`` or empty to fetch
404 all fields in the model
405 :type fields: list | False
406 :returns: a list of records, in the same order as the list of ids
409 Model = request.session.model(model)
410 records = Model.read(ids, fields, request.session.eval_context(request.context))
412 record_map = dict((record['id'], record) for record in records)
414 return [record_map[id] for id in ids if record_map.get(id)]
416 @openerpweb.jsonrequest
417 def load(self, req, model, id, fields):
418 m = req.session.model(model)
420 r = m.read([id], False, req.session.eval_context(req.context))
423 return {'value': value}
425 @openerpweb.jsonrequest
426 def create(self, req, model, data):
427 m = req.session.model(model)
428 r = m.create(data, req.session.eval_context(req.context))
431 @openerpweb.jsonrequest
432 def save(self, req, model, id, data):
433 m = req.session.model(model)
434 r = m.write([id], data, req.session.eval_context(req.context))
437 @openerpweb.jsonrequest
438 def unlink(self, request, model, ids=[]):
439 Model = request.session.model(model)
440 return Model.unlink(ids, request.session.eval_context(request.context))
442 @openerpweb.jsonrequest
443 def call(self, req, model, method, args, domain_id=None, context_id=None):
444 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
445 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
446 c, d = eval_context_and_domain(req.session, context, domain);
447 if(domain_id and len(args) - 1 >= domain_id):
449 if(context_id and len(args) - 1 >= context_id):
452 m = req.session.model(model)
453 r = getattr(m, method)(*args)
456 @openerpweb.jsonrequest
457 def exec_workflow(self, req, model, id, signal):
458 r = req.session.exec_workflow(model, id, signal)
461 @openerpweb.jsonrequest
462 def default_get(self, req, model, fields):
463 m = req.session.model(model)
464 r = m.default_get(fields, req.session.eval_context(req.context))
467 class DataGroup(openerpweb.Controller):
468 _cp_path = "/base/group"
469 @openerpweb.jsonrequest
470 def read(self, request, model, group_by_fields, domain=None):
471 Model = request.session.model(model)
472 context, domain = eval_context_and_domain(request.session, request.context, domain)
474 return Model.read_group(
475 domain or [], False, group_by_fields, 0, False,
476 dict(context, group_by=group_by_fields))
478 class View(openerpweb.Controller):
479 _cp_path = "/base/view"
481 def fields_view_get(self, request, model, view_id, view_type,
482 transform=True, toolbar=False, submenu=False):
483 Model = request.session.model(model)
484 context = request.session.eval_context(request.context)
485 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
486 # todo fme?: check that we should pass the evaluated context here
487 self.process_view(request.session, fvg, context, transform)
490 def process_view(self, session, fvg, context, transform):
492 evaluation_context = session.evaluation_context(context or {})
493 xml = self.transform_view(fvg['arch'], session, evaluation_context)
495 xml = ElementTree.fromstring(fvg['arch'])
496 fvg['arch'] = Xml2Json.convert_element(xml)
497 for field in fvg['fields'].values():
498 if field.has_key('views') and field['views']:
499 for view in field["views"].values():
500 self.process_view(session, view, None, transform)
502 @openerpweb.jsonrequest
503 def add_custom(self, request, view_id, arch):
504 CustomView = request.session.model('ir.ui.view.custom')
506 'user_id': request.session._uid,
509 }, request.session.eval_context(request.context))
510 return {'result': True}
512 @openerpweb.jsonrequest
513 def undo_custom(self, request, view_id, reset=False):
514 CustomView = request.session.model('ir.ui.view.custom')
515 context = request.session.eval_context(request.context)
516 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
517 0, False, False, context)
520 CustomView.unlink(vcustom, context)
522 CustomView.unlink([vcustom[0]], context)
523 return {'result': True}
524 return {'result': False}
526 def normalize_attrs(self, elem, context):
527 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
528 the client only has to deal with @attrs.
530 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
533 :param elem: the current view node (Python object)
534 :type elem: xml.etree.ElementTree.Element
535 :param dict context: evaluation context
537 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
538 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
539 if 'states' in elem.attrib:
540 attrs.setdefault('invisible', [])\
541 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
543 elem.set('attrs', simplejson.dumps(attrs))
544 for a in ['invisible', 'readonly', 'required']:
546 # In the XML we trust
547 avalue = bool(eval(elem.get(a, 'False'),
548 {'context': context or {}}))
553 if a == 'invisible' and 'attrs' in elem.attrib:
554 del elem.attrib['attrs']
556 def transform_view(self, view_string, session, context=None):
557 # transform nodes on the fly via iterparse, instead of
558 # doing it statically on the parsing result
559 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
561 for event, elem in parser:
565 self.normalize_attrs(elem, context)
566 self.parse_domains_and_contexts(elem, session)
569 def parse_domain(self, elem, attr_name, session):
570 """ Parses an attribute of the provided name as a domain, transforms it
571 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
573 :param elem: the node being parsed
574 :type param: xml.etree.ElementTree.Element
575 :param str attr_name: the name of the attribute which should be parsed
576 :param session: Current OpenERP session
577 :type session: openerpweb.openerpweb.OpenERPSession
579 domain = elem.get(attr_name, '').strip()
584 openerpweb.ast.literal_eval(
589 openerpweb.nonliterals.Domain(session, domain))
591 def parse_domains_and_contexts(self, elem, session):
592 """ Converts domains and contexts from the view into Python objects,
593 either literals if they can be parsed by literal_eval or a special
594 placeholder object if the domain or context refers to free variables.
596 :param elem: the current node being parsed
597 :type param: xml.etree.ElementTree.Element
598 :param session: OpenERP session object, used to store and retrieve
600 :type session: openerpweb.openerpweb.OpenERPSession
602 self.parse_domain(elem, 'domain', session)
603 self.parse_domain(elem, 'filter_domain', session)
604 context_string = elem.get('context', '').strip()
608 openerpweb.ast.literal_eval(context_string))
611 openerpweb.nonliterals.Context(
612 session, context_string))
614 class FormView(View):
615 _cp_path = "/base/formview"
617 @openerpweb.jsonrequest
618 def load(self, req, model, view_id, toolbar=False):
619 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
620 return {'fields_view': fields_view}
622 class ListView(View):
623 _cp_path = "/base/listview"
625 @openerpweb.jsonrequest
626 def load(self, req, model, view_id, toolbar=False):
627 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
628 return {'fields_view': fields_view}
630 def process_colors(self, view, row, context):
631 colors = view['arch']['attrs'].get('colors')
638 for pair in colors.split(';')
639 if eval(pair.split(':')[1], dict(context, **row))
644 elif len(color) == 1:
648 class SearchView(View):
649 _cp_path = "/base/searchview"
651 @openerpweb.jsonrequest
652 def load(self, req, model, view_id):
653 fields_view = self.fields_view_get(req, model, view_id, 'search')
654 return {'fields_view': fields_view}
656 @openerpweb.jsonrequest
657 def fields_get(self, req, model):
658 Model = req.session.model(model)
659 fields = Model.fields_get(False, req.session.eval_context(req.context))
660 return {'fields': fields}
662 class Binary(openerpweb.Controller):
663 _cp_path = "/base/binary"
665 @openerpweb.httprequest
666 def image(self, request, session_id, model, id, field, **kw):
667 cherrypy.response.headers['Content-Type'] = 'image/png'
668 Model = request.session.model(model)
669 context = request.session.eval_context(request.context)
672 res = Model.default_get([field], context).get(field, '')
674 res = Model.read([int(id)], [field], context)[0].get(field, '')
675 return base64.decodestring(res)
676 except: # TODO: what's the exception here?
677 return self.placeholder()
678 def placeholder(self):
679 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
681 @openerpweb.httprequest
682 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
683 Model = request.session.model(model)
684 context = request.session.eval_context(request.context)
685 res = Model.read([int(id)], [field, fieldname], context)[0]
686 filecontent = res.get(field, '')
688 raise cherrypy.NotFound
690 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
691 filename = '%s_%s' % (model.replace('.', '_'), id)
693 filename = res.get(fieldname, '') or filename
694 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
695 return base64.decodestring(filecontent)
697 @openerpweb.httprequest
698 def upload(self, request, session_id, callback, ufile=None):
699 cherrypy.response.timeout = 500
701 for key, val in cherrypy.request.headers.iteritems():
702 headers[key.lower()] = val
703 size = int(headers.get('content-length', 0))
704 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
706 out = """<script language="javascript" type="text/javascript">
707 var win = window.top.window,
709 if (typeof(callback) === 'function') {
710 callback.apply(this, %s);
712 win.jQuery('#oe_notification', win.document).notify('create', {
713 title: "Ajax File Upload",
714 text: "Could not find callback"
718 data = ufile.file.read()
719 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
721 args = [False, e.message]
722 return out % (simplejson.dumps(callback), simplejson.dumps(args))
724 @openerpweb.httprequest
725 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
726 cherrypy.response.timeout = 500
727 context = request.session.eval_context(request.context)
728 Model = request.session.model('ir.attachment')
730 out = """<script language="javascript" type="text/javascript">
731 var win = window.top.window,
733 if (typeof(callback) === 'function') {
734 callback.call(this, %s);
737 attachment_id = Model.create({
738 'name': ufile.filename,
739 'datas': base64.encodestring(ufile.file.read()),
744 'filename': ufile.filename,
748 args = { 'error': e.message }
749 return out % (simplejson.dumps(callback), simplejson.dumps(args))
751 class Action(openerpweb.Controller):
752 _cp_path = "/base/action"
754 @openerpweb.jsonrequest
755 def load(self, req, action_id):
756 Actions = req.session.model('ir.actions.actions')
758 context = req.session.eval_context(req.context)
759 context["bin_size"] = False
760 action_type = Actions.read([action_id], ['type'], context)
762 action = req.session.model(action_type[0]['type']).read([action_id], False,
765 value = clean_action(action[0], req.session)
766 return {'result': value}