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", {})
106 @openerpweb.jsonrequest
107 def get_databases_list(self, req):
108 proxy = req.session.proxy("db")
111 return {"db_list": dbs}
113 @openerpweb.jsonrequest
114 def modules(self, req):
115 return {"modules": [name
116 for name, manifest in openerpweb.addons_manifest.iteritems()
117 if manifest.get('active', True)]}
119 @openerpweb.jsonrequest
120 def csslist(self, req, mods='base'):
121 return {'files': self.manifest_glob(mods.split(','), 'css')}
123 @openerpweb.jsonrequest
124 def jslist(self, req, mods='base'):
125 return {'files': self.manifest_glob(mods.split(','), 'js')}
127 def css(self, req, mods='base,base_hello'):
128 files = self.manifest_glob(mods.split(','), 'css')
129 concat = self.concat_files(files)[0]
130 # TODO request set the Date of last modif and Etag
134 def js(self, req, mods='base,base_hello'):
135 files = self.manifest_glob(mods.split(','), 'js')
136 concat = self.concat_files(files)[0]
137 # TODO request set the Date of last modif and Etag
141 @openerpweb.jsonrequest
142 def eval_domain_and_context(self, req, contexts, domains,
144 """ Evaluates sequences of domains and contexts, composing them into
145 a single context, domain or group_by sequence.
147 :param list contexts: list of contexts to merge together. Contexts are
148 evaluated in sequence, all previous contexts
149 are part of their own evaluation context
150 (starting at the session context).
151 :param list domains: list of domains to merge together. Domains are
152 evaluated in sequence and appended to one another
153 (implicit AND), their evaluation domain is the
154 result of merging all contexts.
155 :param list group_by_seq: list of domains (which may be in a different
156 order than the ``contexts`` parameter),
157 evaluated in sequence, their ``'group_by'``
158 key is extracted if they have one.
163 the global context created by merging all of
167 the concatenation of all domains
170 a list of fields to group by, potentially empty (in which case
171 no group by should be performed)
173 return _eval_domain_and_context(req, contexts, domains, group_by_seq)
175 @openerpweb.jsonrequest
176 def save_session_action(self, req, the_action):
178 This method store an action object in the session object and returns an integer
179 identifying that action. The method get_session_action() can be used to get
182 :param the_action: The action to save in the session.
183 :type the_action: anything
184 :return: A key identifying the saved action.
187 saved_actions = cherrypy.session.get('saved_actions')
188 if not saved_actions:
189 saved_actions = {"next":0, "actions":{}}
190 cherrypy.session['saved_actions'] = saved_actions
191 # we don't allow more than 10 stored actions
192 if len(saved_actions["actions"]) >= 10:
193 del saved_actions["actions"][min(saved_actions["actions"].keys())]
194 key = saved_actions["next"]
195 saved_actions["actions"][key] = the_action
196 saved_actions["next"] = key + 1
199 @openerpweb.jsonrequest
200 def get_session_action(self, req, key):
202 Gets back a previously saved action. This method can return None if the action
203 was saved since too much time (this case should be handled in a smart way).
205 :param key: The key given by save_session_action()
207 :return: The saved action or None.
210 saved_actions = cherrypy.session.get('saved_actions')
211 if not saved_actions:
213 return saved_actions["actions"].get(key)
215 def _eval_domain_and_context(req, contexts, domains, group_by_seq=None):
216 context = req.session.eval_contexts(contexts)
217 domain = req.session.eval_domains(domains, context)
219 group_by_sequence = []
220 for candidate in (group_by_seq or []):
221 ctx = req.session.eval_context(candidate, context)
222 group_by = ctx.get('group_by')
225 elif isinstance(group_by, basestring):
226 group_by_sequence.append(group_by)
228 group_by_sequence.extend(group_by)
233 'group_by': group_by_sequence
236 def load_actions_from_ir_values(req, key, key2, models, meta, context):
237 Values = req.session.model('ir.values')
238 actions = Values.get(key, key2, models, meta, context)
240 for _, _, action in actions:
241 clean_action(action, req.session)
245 def clean_action(action, session):
246 # values come from the server, we can just eval them
247 if isinstance(action['context'], basestring):
248 action['context'] = eval(
250 session.evaluation_context()) or {}
252 if isinstance(action['domain'], basestring):
253 action['domain'] = eval(
255 session.evaluation_context(
256 action['context'])) or []
257 if not action.has_key('flags'):
258 # Set empty flags dictionary for web client.
259 action['flags'] = dict()
260 return fix_view_modes(action)
262 def fix_view_modes(action):
263 """ For historical reasons, OpenERP has weird dealings in relation to
264 view_mode and the view_type attribute (on window actions):
266 * one of the view modes is ``tree``, which stands for both list views
268 * the choice is made by checking ``view_type``, which is either
269 ``form`` for a list view or ``tree`` for an actual tree view
271 This methods simply folds the view_type into view_mode by adding a
272 new view mode ``list`` which is the result of the ``tree`` view_mode
273 in conjunction with the ``form`` view_type.
275 TODO: this should go into the doc, some kind of "peculiarities" section
277 :param dict action: an action descriptor
278 :returns: nothing, the action is modified in place
280 if action.pop('view_type') != 'form':
283 action['view_mode'] = ','.join(
284 mode if mode != 'tree' else 'list'
285 for mode in action['view_mode'].split(','))
287 [id, mode if mode != 'tree' else 'list']
288 for id, mode in action['views']
292 class Menu(openerpweb.Controller):
293 _cp_path = "/base/menu"
295 @openerpweb.jsonrequest
297 return {'data': self.do_load(req)}
299 def do_load(self, req):
300 """ Loads all menu items (all applications and their sub-menus).
302 :param req: A request object, with an OpenERP session attribute
303 :type req: < session -> OpenERPSession >
304 :return: the menu root
305 :rtype: dict('children': menu_nodes)
307 Menus = req.session.model('ir.ui.menu')
308 # menus are loaded fully unlike a regular tree view, cause there are
309 # less than 512 items
310 menu_ids = Menus.search([])
311 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
312 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
313 menu_items.append(menu_root)
315 # make a tree using parent_id
316 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
317 for menu_item in menu_items:
318 if menu_item['parent_id']:
319 parent = menu_item['parent_id'][0]
322 if parent in menu_items_map:
323 menu_items_map[parent].setdefault(
324 'children', []).append(menu_item)
326 # sort by sequence a tree using parent_id
327 for menu_item in menu_items:
328 menu_item.setdefault('children', []).sort(
329 key=lambda x:x["sequence"])
333 @openerpweb.jsonrequest
334 def action(self, req, menu_id):
335 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
336 [('ir.ui.menu', menu_id)], False, {})
338 return {"action": actions}
340 class DataSet(openerpweb.Controller):
341 _cp_path = "/base/dataset"
343 @openerpweb.jsonrequest
344 def fields(self, req, model):
345 return {'fields': req.session.model(model).fields_get()}
347 @openerpweb.jsonrequest
348 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
349 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
350 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
351 """ Performs a search() followed by a read() (if needed) using the
352 provided search criteria
354 :param request: a JSON-RPC request object
355 :type request: openerpweb.JsonRequest
356 :param str model: the name of the model to search on
357 :param fields: a list of the fields to return in the result records
359 :param int offset: from which index should the results start being returned
360 :param int limit: the maximum number of records to return
361 :param list domain: the search domain for the query
362 :param list sort: sorting directives
363 :returns: a list of result records
366 Model = request.session.model(model)
368 ids = Model.search(domain or [], offset or 0, limit or False,
369 sort or False, request.context)
371 if fields and fields == ['id']:
372 # shortcut read if we only want the ids
373 return map(lambda id: {'id': id}, ids)
375 reads = Model.read(ids, fields or False, request.context)
376 reads.sort(key=lambda obj: ids.index(obj['id']))
379 @openerpweb.jsonrequest
380 def read(self, request, model, ids, fields=False):
381 return self.do_search_read(request, model, ids, fields)
383 @openerpweb.jsonrequest
384 def get(self, request, model, ids, fields=False):
385 return self.do_get(request, model, ids, fields)
386 def do_get(self, request, model, ids, fields=False):
387 """ Fetches and returns the records of the model ``model`` whose ids
390 The results are in the same order as the inputs, but elements may be
391 missing (if there is no record left for the id)
393 :param request: the JSON-RPC2 request object
394 :type request: openerpweb.JsonRequest
395 :param model: the model to read from
397 :param ids: a list of identifiers
399 :param fields: a list of fields to fetch, ``False`` or empty to fetch
400 all fields in the model
401 :type fields: list | False
402 :returns: a list of records, in the same order as the list of ids
405 Model = request.session.model(model)
406 records = Model.read(ids, fields, request.context)
408 record_map = dict((record['id'], record) for record in records)
410 return [record_map[id] for id in ids if record_map.get(id)]
411 @openerpweb.jsonrequest
413 def load(self, req, model, id, fields):
414 m = req.session.model(model)
419 return {'value': value}
421 @openerpweb.jsonrequest
422 def create(self, req, model, data, context={}):
423 m = req.session.model(model)
424 r = m.create(data, context)
427 @openerpweb.jsonrequest
428 def save(self, req, model, id, data, context={}):
429 m = req.session.model(model)
430 r = m.write([id], data, context)
433 @openerpweb.jsonrequest
434 def unlink(self, request, model, ids=[]):
435 Model = request.session.model(model)
436 return Model.unlink(ids)
438 @openerpweb.jsonrequest
439 def call(self, req, model, method, args, domain_id=None, context_id=None):
440 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
441 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
442 res = _eval_domain_and_context(req, [context], [domain]);
443 if(domain_id and len(args) - 1 >= domain_id):
444 args[context_id] = res["context"]
445 if(context_id and len(args) - 1 >= context_id):
446 args[domain_id] = res["domain"]
448 m = req.session.model(model)
449 r = getattr(m, method)(*args)
452 @openerpweb.jsonrequest
453 def exec_workflow(self, req, model, id, signal):
454 r = req.session.exec_workflow(model, id, signal)
457 @openerpweb.jsonrequest
458 def default_get(self, req, model, fields, context={}):
459 m = req.session.model(model)
460 r = m.default_get(fields, context)
463 class DataGroup(openerpweb.Controller):
464 _cp_path = "/base/group"
465 @openerpweb.jsonrequest
466 def read(self, request, model, group_by_fields, domain=None):
467 Model = request.session.model(model)
469 return Model.read_group(
470 domain or [], False, group_by_fields, 0, False,
471 dict(request.context, group_by=group_by_fields))
473 class View(openerpweb.Controller):
474 _cp_path = "/base/view"
476 def fields_view_get(self, request, model, view_id, view_type,
477 transform=True, toolbar=False, submenu=False):
478 Model = request.session.model(model)
479 fvg = Model.fields_view_get(view_id, view_type, request.context,
481 self.process_view(request.session, fvg, request.context, transform)
484 def process_view(self, session, fvg, context, transform):
486 evaluation_context = session.evaluation_context(context or {})
487 xml = self.transform_view(fvg['arch'], session, evaluation_context)
489 xml = ElementTree.fromstring(fvg['arch'])
490 fvg['arch'] = Xml2Json.convert_element(xml)
491 for field in fvg['fields'].values():
492 if field.has_key('views') and field['views']:
493 for view in field["views"].values():
494 self.process_view(session, view, None, transform)
496 @openerpweb.jsonrequest
497 def add_custom(self, request, view_id, arch):
498 CustomView = request.session.model('ir.ui.view.custom')
500 'user_id': request.session._uid,
504 return {'result': True}
506 @openerpweb.jsonrequest
507 def undo_custom(self, request, view_id, reset=False):
508 CustomView = request.session.model('ir.ui.view.custom')
509 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)])
512 CustomView.unlink(vcustom)
514 CustomView.unlink([vcustom[0]])
515 return {'result': True}
516 return {'result': False}
518 def normalize_attrs(self, elem, context):
519 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
520 the client only has to deal with @attrs.
522 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
525 :param elem: the current view node (Python object)
526 :type elem: xml.etree.ElementTree.Element
527 :param dict context: evaluation context
529 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
530 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
531 if 'states' in elem.attrib:
532 attrs.setdefault('invisible', [])\
533 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
535 elem.set('attrs', simplejson.dumps(attrs))
536 for a in ['invisible', 'readonly', 'required']:
538 # In the XML we trust
539 avalue = bool(eval(elem.get(a, 'False'),
540 {'context': context or {}}))
545 if a == 'invisible' and 'attrs' in elem.attrib:
546 del elem.attrib['attrs']
548 def transform_view(self, view_string, session, context=None):
549 # transform nodes on the fly via iterparse, instead of
550 # doing it statically on the parsing result
551 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
553 for event, elem in parser:
557 self.normalize_attrs(elem, context)
558 self.parse_domains_and_contexts(elem, session)
561 def parse_domain(self, elem, attr_name, session):
562 """ Parses an attribute of the provided name as a domain, transforms it
563 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
565 :param elem: the node being parsed
566 :type param: xml.etree.ElementTree.Element
567 :param str attr_name: the name of the attribute which should be parsed
568 :param session: Current OpenERP session
569 :type session: openerpweb.openerpweb.OpenERPSession
571 domain = elem.get(attr_name, '').strip()
576 openerpweb.ast.literal_eval(
581 openerpweb.nonliterals.Domain(session, domain))
583 def parse_domains_and_contexts(self, elem, session):
584 """ Converts domains and contexts from the view into Python objects,
585 either literals if they can be parsed by literal_eval or a special
586 placeholder object if the domain or context refers to free variables.
588 :param elem: the current node being parsed
589 :type param: xml.etree.ElementTree.Element
590 :param session: OpenERP session object, used to store and retrieve
592 :type session: openerpweb.openerpweb.OpenERPSession
594 self.parse_domain(elem, 'domain', session)
595 self.parse_domain(elem, 'filter_domain', session)
596 context_string = elem.get('context', '').strip()
600 openerpweb.ast.literal_eval(context_string))
603 openerpweb.nonliterals.Context(
604 session, context_string))
606 class FormView(View):
607 _cp_path = "/base/formview"
609 @openerpweb.jsonrequest
610 def load(self, req, model, view_id, toolbar=False):
611 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
612 return {'fields_view': fields_view}
614 class ListView(View):
615 _cp_path = "/base/listview"
617 @openerpweb.jsonrequest
618 def load(self, req, model, view_id, toolbar=False):
619 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
620 return {'fields_view': fields_view}
622 def process_colors(self, view, row, context):
623 colors = view['arch']['attrs'].get('colors')
630 for pair in colors.split(';')
631 if eval(pair.split(':')[1], dict(context, **row))
636 elif len(color) == 1:
640 class SearchView(View):
641 _cp_path = "/base/searchview"
643 @openerpweb.jsonrequest
644 def load(self, req, model, view_id):
645 fields_view = self.fields_view_get(req, model, view_id, 'search')
646 return {'fields_view': fields_view}
648 @openerpweb.jsonrequest
649 def fields_get(self, req, model):
650 Model = req.session.model(model)
651 fields = Model.fields_get()
652 return {'fields': fields}
654 class Binary(openerpweb.Controller):
655 _cp_path = "/base/binary"
657 @openerpweb.httprequest
658 def image(self, request, session_id, model, id, field, **kw):
659 cherrypy.response.headers['Content-Type'] = 'image/png'
660 Model = request.session.model(model)
663 res = Model.default_get([field], request.context).get(field, '')
665 res = Model.read([int(id)], [field], request.context)[0].get(field, '')
666 return base64.decodestring(res)
667 except: # TODO: what's the exception here?
668 return self.placeholder()
669 def placeholder(self):
670 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
672 @openerpweb.httprequest
673 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
674 Model = request.session.model(model)
675 res = Model.read([int(id)], [field, fieldname])[0]
676 filecontent = res.get(field, '')
678 raise cherrypy.NotFound
680 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
681 filename = '%s_%s' % (model.replace('.', '_'), id)
683 filename = res.get(fieldname, '') or filename
684 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
685 return base64.decodestring(filecontent)
687 @openerpweb.httprequest
688 def upload(self, request, session_id, callback, ufile=None):
689 cherrypy.response.timeout = 500
691 for key, val in cherrypy.request.headers.iteritems():
692 headers[key.lower()] = val
693 size = int(headers.get('content-length', 0))
694 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
696 out = """<script language="javascript" type="text/javascript">
697 var win = window.top.window,
699 if (typeof(callback) === 'function') {
700 callback.apply(this, %s);
702 win.jQuery('#oe_notification', win.document).notify('create', {
703 title: "Ajax File Upload",
704 text: "Could not find callback"
708 data = ufile.file.read()
709 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
711 args = [False, e.message]
712 return out % (simplejson.dumps(callback), simplejson.dumps(args))
714 @openerpweb.httprequest
715 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
716 cherrypy.response.timeout = 500
717 Model = request.session.model('ir.attachment')
719 out = """<script language="javascript" type="text/javascript">
720 var win = window.top.window,
722 if (typeof(callback) === 'function') {
723 callback.call(this, %s);
726 attachment_id = Model.create({
727 'name': ufile.filename,
728 'datas': base64.encodestring(ufile.file.read()),
733 'filename': ufile.filename,
737 args = { 'error': e.message }
738 return out % (simplejson.dumps(callback), simplejson.dumps(args))
740 class Action(openerpweb.Controller):
741 _cp_path = "/base/action"
743 @openerpweb.jsonrequest
744 def load(self, req, action_id):
745 Actions = req.session.model('ir.actions.actions')
747 action_type = Actions.read([action_id], ['type'], req.session.context)
749 action = req.session.model(action_type[0]['type']).read([action_id], False, req.session.context)
751 value = clean_action(action[0], req.session)
752 return {'result': value}