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 get_databases_list(self, req):
104 proxy = req.session.proxy("db")
107 return {"db_list": dbs}
109 @openerpweb.jsonrequest
110 def modules(self, req):
111 return {"modules": [name
112 for name, manifest in openerpweb.addons_manifest.iteritems()
113 if manifest.get('active', True)]}
115 @openerpweb.jsonrequest
116 def csslist(self, req, mods='base'):
117 return {'files': self.manifest_glob(mods.split(','), 'css')}
119 @openerpweb.jsonrequest
120 def jslist(self, req, mods='base'):
121 return {'files': self.manifest_glob(mods.split(','), 'js')}
123 def css(self, req, mods='base,base_hello'):
124 files = self.manifest_glob(mods.split(','), 'css')
125 concat = self.concat_files(files)[0]
126 # TODO request set the Date of last modif and Etag
130 def js(self, req, mods='base,base_hello'):
131 files = self.manifest_glob(mods.split(','), 'js')
132 concat = self.concat_files(files)[0]
133 # TODO request set the Date of last modif and Etag
137 @openerpweb.jsonrequest
138 def eval_domain_and_context(self, req, contexts, domains,
140 """ Evaluates sequences of domains and contexts, composing them into
141 a single context, domain or group_by sequence.
143 :param list contexts: list of contexts to merge together. Contexts are
144 evaluated in sequence, all previous contexts
145 are part of their own evaluation context
146 (starting at the session context).
147 :param list domains: list of domains to merge together. Domains are
148 evaluated in sequence and appended to one another
149 (implicit AND), their evaluation domain is the
150 result of merging all contexts.
151 :param list group_by_seq: list of domains (which may be in a different
152 order than the ``contexts`` parameter),
153 evaluated in sequence, their ``'group_by'``
154 key is extracted if they have one.
159 the global context created by merging all of
163 the concatenation of all domains
166 a list of fields to group by, potentially empty (in which case
167 no group by should be performed)
169 context = req.session.eval_contexts(contexts)
170 domain = req.session.eval_domains(domains, context)
172 group_by_sequence = []
173 for candidate in (group_by_seq or []):
174 ctx = req.session.eval_context(candidate, context)
175 group_by = ctx.get('group_by')
178 elif isinstance(group_by, basestring):
179 group_by_sequence.append(group_by)
181 group_by_sequence.extend(group_by)
186 'group_by': group_by_sequence
189 @openerpweb.jsonrequest
190 def save_session_action(self, req, the_action):
192 This method store an action object in the session object and returns an integer
193 identifying that action. The method get_session_action() can be used to get
196 :param the_action: The action to save in the session.
197 :type the_action: anything
198 :return: A key identifying the saved action.
201 saved_actions = cherrypy.session.get('saved_actions')
202 if not saved_actions:
203 saved_actions = {"next":0, "actions":{}}
204 cherrypy.session['saved_actions'] = saved_actions
205 # we don't allow more than 10 stored actions
206 if len(saved_actions["actions"]) >= 10:
207 del saved_actions["actions"][min(saved_actions["actions"].keys())]
208 key = saved_actions["next"]
209 saved_actions["actions"][key] = the_action
210 saved_actions["next"] = key + 1
213 @openerpweb.jsonrequest
214 def get_session_action(self, req, key):
216 Gets back a previously saved action. This method can return None if the action
217 was saved since too much time (this case should be handled in a smart way).
219 :param key: The key given by save_session_action()
221 :return: The saved action or None.
224 saved_actions = cherrypy.session.get('saved_actions')
225 if not saved_actions:
227 return saved_actions["actions"].get(key)
230 def load_actions_from_ir_values(req, key, key2, models, meta, context):
231 Values = req.session.model('ir.values')
232 actions = Values.get(key, key2, models, meta, context)
234 for _, _, action in actions:
235 clean_action(action, req.session)
239 def clean_action(action, session):
240 # values come from the server, we can just eval them
241 if isinstance(action['context'], basestring):
242 action['context'] = eval(
244 session.evaluation_context()) or {}
246 if isinstance(action['domain'], basestring):
247 action['domain'] = eval(
249 session.evaluation_context(
250 action['context'])) or []
251 if not action.has_key('flags'):
252 # Set empty flags dictionary for web client.
253 action['flags'] = dict()
254 return fix_view_modes(action)
256 def fix_view_modes(action):
257 """ For historical reasons, OpenERP has weird dealings in relation to
258 view_mode and the view_type attribute (on window actions):
260 * one of the view modes is ``tree``, which stands for both list views
262 * the choice is made by checking ``view_type``, which is either
263 ``form`` for a list view or ``tree`` for an actual tree view
265 This methods simply folds the view_type into view_mode by adding a
266 new view mode ``list`` which is the result of the ``tree`` view_mode
267 in conjunction with the ``form`` view_type.
269 TODO: this should go into the doc, some kind of "peculiarities" section
271 :param dict action: an action descriptor
272 :returns: nothing, the action is modified in place
274 if action.pop('view_type') != 'form':
277 action['view_mode'] = ','.join(
278 mode if mode != 'tree' else 'list'
279 for mode in action['view_mode'].split(','))
281 [id, mode if mode != 'tree' else 'list']
282 for id, mode in action['views']
286 class Menu(openerpweb.Controller):
287 _cp_path = "/base/menu"
289 @openerpweb.jsonrequest
291 return {'data': self.do_load(req)}
293 def do_load(self, req):
294 """ Loads all menu items (all applications and their sub-menus).
296 :param req: A request object, with an OpenERP session attribute
297 :type req: < session -> OpenERPSession >
298 :return: the menu root
299 :rtype: dict('children': menu_nodes)
301 Menus = req.session.model('ir.ui.menu')
302 # menus are loaded fully unlike a regular tree view, cause there are
303 # less than 512 items
304 menu_ids = Menus.search([])
305 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
306 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
307 menu_items.append(menu_root)
309 # make a tree using parent_id
310 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
311 for menu_item in menu_items:
312 if menu_item['parent_id']:
313 parent = menu_item['parent_id'][0]
316 if parent in menu_items_map:
317 menu_items_map[parent].setdefault(
318 'children', []).append(menu_item)
320 # sort by sequence a tree using parent_id
321 for menu_item in menu_items:
322 menu_item.setdefault('children', []).sort(
323 key=lambda x:x["sequence"])
327 @openerpweb.jsonrequest
328 def action(self, req, menu_id):
329 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
330 [('ir.ui.menu', menu_id)], False, {})
332 return {"action": actions}
334 class DataSet(openerpweb.Controller):
335 _cp_path = "/base/dataset"
337 @openerpweb.jsonrequest
338 def fields(self, req, model):
339 return {'fields': req.session.model(model).fields_get()}
341 @openerpweb.jsonrequest
342 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
343 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
344 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
345 """ Performs a search() followed by a read() (if needed) using the
346 provided search criteria
348 :param request: a JSON-RPC request object
349 :type request: openerpweb.JsonRequest
350 :param str model: the name of the model to search on
351 :param fields: a list of the fields to return in the result records
353 :param int offset: from which index should the results start being returned
354 :param int limit: the maximum number of records to return
355 :param list domain: the search domain for the query
356 :param list sort: sorting directives
357 :returns: a list of result records
360 Model = request.session.model(model)
362 ids = Model.search(domain or [], offset or 0, limit or False,
363 sort or False, request.context)
365 if fields and fields == ['id']:
366 # shortcut read if we only want the ids
367 return map(lambda id: {'id': id}, ids)
369 reads = Model.read(ids, fields or False, request.context)
370 reads.sort(key=lambda obj: ids.index(obj['id']))
373 @openerpweb.jsonrequest
374 def read(self, request, model, ids, fields=False):
375 return self.do_search_read(request, model, ids, fields)
377 @openerpweb.jsonrequest
378 def get(self, request, model, ids, fields=False):
379 return self.do_get(request, model, ids, fields)
381 def do_get(self, request, model, ids, fields=False):
382 """ Fetches and returns the records of the model ``model`` whose ids
385 The results are in the same order as the inputs, but elements may be
386 missing (if there is no record left for the id)
388 :param request: the JSON-RPC2 request object
389 :type request: openerpweb.JsonRequest
390 :param model: the model to read from
392 :param ids: a list of identifiers
394 :param fields: a list of fields to fetch, ``False`` or empty to fetch
395 all fields in the model
396 :type fields: list | False
397 :returns: a list of records, in the same order as the list of ids
400 Model = request.session.model(model)
401 records = Model.read(ids, fields, request.context)
403 record_map = dict((record['id'], record) for record in records)
405 return [record_map[id] for id in ids if record_map.get(id)]
407 @openerpweb.jsonrequest
408 def load(self, req, model, id, fields):
409 m = req.session.model(model)
414 return {'value': value}
416 @openerpweb.jsonrequest
417 def create(self, req, model, data, context={}):
418 m = req.session.model(model)
419 r = m.create(data, context)
422 @openerpweb.jsonrequest
423 def save(self, req, model, id, data, context={}):
424 m = req.session.model(model)
425 r = m.write([id], data, context)
428 @openerpweb.jsonrequest
429 def unlink(self, request, model, ids=[]):
430 Model = request.session.model(model)
431 return Model.unlink(ids)
433 @openerpweb.jsonrequest
434 def call(self, req, model, method, args):
435 m = req.session.model(model)
436 r = getattr(m, method)(*args)
439 @openerpweb.jsonrequest
440 def exec_workflow(self, req, model, id, signal):
441 r = req.session.exec_workflow(model, id, signal)
444 @openerpweb.jsonrequest
445 def default_get(self, req, model, fields, context={}):
446 m = req.session.model(model)
447 r = m.default_get(fields, context)
450 @openerpweb.jsonrequest
451 def name_search(self, req, model, search_str, domain=[], context={}):
452 m = req.session.model(model)
453 r = m.name_search(search_str+'%', domain, '=ilike', context)
456 class DataGroup(openerpweb.Controller):
457 _cp_path = "/base/group"
458 @openerpweb.jsonrequest
459 def read(self, request, model, group_by_fields, domain=None):
460 Model = request.session.model(model)
462 return Model.read_group(
463 domain or [], False, group_by_fields, 0, False,
464 dict(request.context, group_by=group_by_fields))
466 class View(openerpweb.Controller):
467 _cp_path = "/base/view"
469 def fields_view_get(self, request, model, view_id, view_type,
470 transform=True, toolbar=False, submenu=False):
471 Model = request.session.model(model)
472 fvg = Model.fields_view_get(view_id, view_type, request.context,
474 self.process_view(request.session, fvg, request.context, transform)
477 def process_view(self, session, fvg, context, transform):
479 evaluation_context = session.evaluation_context(context or {})
480 xml = self.transform_view(fvg['arch'], session, evaluation_context)
482 xml = ElementTree.fromstring(fvg['arch'])
483 fvg['arch'] = Xml2Json.convert_element(xml)
484 for field in fvg['fields'].values():
485 if field.has_key('views') and field['views']:
486 for view in field["views"].values():
487 self.process_view(session, view, None, transform)
489 @openerpweb.jsonrequest
490 def add_custom(self, request, view_id, arch):
491 CustomView = request.session.model('ir.ui.view.custom')
493 'user_id': request.session._uid,
497 return {'result': True}
499 @openerpweb.jsonrequest
500 def undo_custom(self, request, view_id, reset=False):
501 CustomView = request.session.model('ir.ui.view.custom')
502 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)])
505 CustomView.unlink(vcustom)
507 CustomView.unlink([vcustom[0]])
508 return {'result': True}
509 return {'result': False}
511 def normalize_attrs(self, elem, context):
512 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
513 the client only has to deal with @attrs.
515 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
518 :param elem: the current view node (Python object)
519 :type elem: xml.etree.ElementTree.Element
520 :param dict context: evaluation context
522 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
523 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
524 if 'states' in elem.attrib:
525 attrs.setdefault('invisible', [])\
526 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
528 elem.set('attrs', simplejson.dumps(attrs))
529 for a in ['invisible', 'readonly', 'required']:
531 # In the XML we trust
532 avalue = bool(eval(elem.get(a, 'False'),
533 {'context': context or {}}))
538 if a == 'invisible' and 'attrs' in elem.attrib:
539 del elem.attrib['attrs']
541 def transform_view(self, view_string, session, context=None):
542 # transform nodes on the fly via iterparse, instead of
543 # doing it statically on the parsing result
544 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
546 for event, elem in parser:
550 self.normalize_attrs(elem, context)
551 self.parse_domains_and_contexts(elem, session)
554 def parse_domain(self, elem, attr_name, session):
555 """ Parses an attribute of the provided name as a domain, transforms it
556 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
558 :param elem: the node being parsed
559 :type param: xml.etree.ElementTree.Element
560 :param str attr_name: the name of the attribute which should be parsed
561 :param session: Current OpenERP session
562 :type session: openerpweb.openerpweb.OpenERPSession
564 domain = elem.get(attr_name, '').strip()
569 openerpweb.ast.literal_eval(
574 openerpweb.nonliterals.Domain(session, domain))
576 def parse_domains_and_contexts(self, elem, session):
577 """ Converts domains and contexts from the view into Python objects,
578 either literals if they can be parsed by literal_eval or a special
579 placeholder object if the domain or context refers to free variables.
581 :param elem: the current node being parsed
582 :type param: xml.etree.ElementTree.Element
583 :param session: OpenERP session object, used to store and retrieve
585 :type session: openerpweb.openerpweb.OpenERPSession
587 self.parse_domain(elem, 'domain', session)
588 self.parse_domain(elem, 'filter_domain', session)
589 context_string = elem.get('context', '').strip()
593 openerpweb.ast.literal_eval(context_string))
596 openerpweb.nonliterals.Context(
597 session, context_string))
599 class FormView(View):
600 _cp_path = "/base/formview"
602 @openerpweb.jsonrequest
603 def load(self, req, model, view_id, toolbar=False):
604 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
605 return {'fields_view': fields_view}
607 class ListView(View):
608 _cp_path = "/base/listview"
610 @openerpweb.jsonrequest
611 def load(self, req, model, view_id, toolbar=False):
612 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
613 return {'fields_view': fields_view}
615 def process_colors(self, view, row, context):
616 colors = view['arch']['attrs'].get('colors')
623 for pair in colors.split(';')
624 if eval(pair.split(':')[1], dict(context, **row))
629 elif len(color) == 1:
633 class SearchView(View):
634 _cp_path = "/base/searchview"
636 @openerpweb.jsonrequest
637 def load(self, req, model, view_id):
638 fields_view = self.fields_view_get(req, model, view_id, 'search')
639 return {'fields_view': fields_view}
641 @openerpweb.jsonrequest
642 def fields_get(self, req, model):
643 Model = req.session.model(model)
644 fields = Model.fields_get()
645 return {'fields': fields}
647 class Binary(openerpweb.Controller):
648 _cp_path = "/base/binary"
650 @openerpweb.httprequest
651 def image(self, request, session_id, model, id, field, **kw):
652 cherrypy.response.headers['Content-Type'] = 'image/png'
653 Model = request.session.model(model)
656 res = Model.default_get([field], request.context).get(field, '')
658 res = Model.read([int(id)], [field], request.context)[0].get(field, '')
659 return base64.decodestring(res)
660 except: # TODO: what's the exception here?
661 return self.placeholder()
662 def placeholder(self):
663 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
665 @openerpweb.httprequest
666 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
667 Model = request.session.model(model)
668 res = Model.read([int(id)], [field, fieldname])[0]
669 filecontent = res.get(field, '')
671 raise cherrypy.NotFound
673 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
674 filename = '%s_%s' % (model.replace('.', '_'), id)
676 filename = res.get(fieldname, '') or filename
677 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
678 return base64.decodestring(filecontent)
680 @openerpweb.httprequest
681 def upload(self, request, session_id, callback, ufile=None):
682 cherrypy.response.timeout = 500
684 for key, val in cherrypy.request.headers.iteritems():
685 headers[key.lower()] = val
686 size = int(headers.get('content-length', 0))
687 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
689 out = """<script language="javascript" type="text/javascript">
690 var win = window.top.window,
692 if (typeof(callback) === 'function') {
693 callback.apply(this, %s);
695 win.jQuery('#oe_notification', win.document).notify('create', {
696 title: "Ajax File Upload",
697 text: "Could not find callback"
701 data = ufile.file.read()
702 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
704 args = [False, e.message]
705 return out % (simplejson.dumps(callback), simplejson.dumps(args))
707 @openerpweb.httprequest
708 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
709 cherrypy.response.timeout = 500
710 Model = request.session.model('ir.attachment')
712 out = """<script language="javascript" type="text/javascript">
713 var win = window.top.window,
715 if (typeof(callback) === 'function') {
716 callback.call(this, %s);
719 attachment_id = Model.create({
720 'name': ufile.filename,
721 'datas': base64.encodestring(ufile.file.read()),
726 'filename': ufile.filename,
730 args = { 'error': e.message }
731 return out % (simplejson.dumps(callback), simplejson.dumps(args))
733 class Action(openerpweb.Controller):
734 _cp_path = "/base/action"
736 @openerpweb.jsonrequest
737 def load(self, req, action_id):
738 Actions = req.session.model('ir.actions.actions')
740 action_type = Actions.read([action_id], ['type'], req.session.context)
742 action = req.session.model(action_type[0]['type']).read([action_id], False, req.session.context)
744 value = clean_action(action[0], req.session)
745 return {'result': value}
746 class TreeView(View):
747 _cp_path = "/base/treeview"
749 @openerpweb.jsonrequest
750 def load(self, req, model, view_id, toolbar=False):
751 Model = req.session.model(model)
752 fields = Model.fields_get()
753 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
754 return {'field_parent': fields_view, 'fields' : fields}