1 # -*- coding: utf-8 -*-
4 from xml.etree import ElementTree
5 from cStringIO import StringIO
11 import openerpweb.nonliterals
15 # Should move to openerpweb.Xml2Json
18 # Simple and straightforward XML-to-JSON converter in Python
21 # URL: http://code.google.com/p/xml2json-direct/
23 def convert_to_json(s):
24 return simplejson.dumps(
25 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
28 def convert_to_structure(s):
29 root = ElementTree.fromstring(s)
30 return Xml2Json.convert_element(root)
33 def convert_element(el, skip_whitespaces=True):
36 ns, name = el.tag.rsplit("}", 1)
38 res["namespace"] = ns[1:]
42 for k, v in el.items():
45 if el.text and (not skip_whitespaces or el.text.strip() != ''):
48 kids.append(Xml2Json.convert_element(kid))
49 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
51 res["children"] = kids
54 #----------------------------------------------------------
55 # OpenERP Web base Controllers
56 #----------------------------------------------------------
58 class Session(openerpweb.Controller):
59 _cp_path = "/base/session"
61 def manifest_glob(self, addons, key):
64 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
67 resource_path[len(openerpweb.path_addons):]
68 for pattern in globlist
69 for resource_path in glob.glob(os.path.join(
70 openerpweb.path_addons, addon, pattern))
74 def concat_files(self, file_list):
75 """ Concatenate file content
76 return (concat,timestamp)
77 concat: concatenation of file content
78 timestamp: max(os.path.getmtime of file_list)
80 root = openerpweb.path_root
84 fname = os.path.join(root, i)
85 ftime = os.path.getmtime(fname)
86 if ftime > files_timestamp:
87 files_timestamp = ftime
88 files_content = open(fname).read()
89 files_concat = "".join(files_content)
92 @openerpweb.jsonrequest
93 def login(self, req, db, login, password):
94 req.session.login(db, login, password)
97 "session_id": req.session_id,
98 "uid": req.session._uid,
101 @openerpweb.jsonrequest
102 def sc_list(self, req):
103 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
104 req.session.eval_context(req.context))
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'):
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'):
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 context, domain = eval_context_and_domain(req.session,
174 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
175 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
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 # should we give the evaluated context as an evaluation context to the domain?
237 e_domain = session.eval_domain(domain or [])
239 return e_context, e_domain
241 def load_actions_from_ir_values(req, key, key2, models, meta, context):
242 Values = req.session.model('ir.values')
243 actions = Values.get(key, key2, models, meta, context)
245 return [(id, name, clean_action(action, req.session))
246 for id, name, action in actions]
248 def clean_action(action, session):
249 if action['type'] != 'ir.actions.act_window':
251 # values come from the server, we can just eval them
252 if isinstance(action.get('context', None), basestring):
253 action['context'] = eval(
255 session.evaluation_context()) or {}
257 if isinstance(action.get('domain', None), basestring):
258 action['domain'] = eval(
260 session.evaluation_context(
261 action['context'])) or []
262 if 'flags' not in action:
263 # Set empty flags dictionary for web client.
264 action['flags'] = dict()
265 return fix_view_modes(action)
267 def generate_views(action):
269 While the server generates a sequence called "views" computing dependencies
270 between a bunch of stuff for views coming directly from the database
271 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
272 to return custom view dictionaries generated on the fly.
274 In that case, there is no ``views`` key available on the action.
276 Since the web client relies on ``action['views']``, generate it here from
277 ``view_mode`` and ``view_id``.
279 Currently handles two different cases:
281 * no view_id, multiple view_mode
282 * single view_id, single view_mode
284 :param dict action: action descriptor dictionary to generate a views key for
286 view_id = action.get('view_id', False)
287 if isinstance(view_id, (list, tuple)):
290 # providing at least one view mode is a requirement, not an option
291 view_modes = action['view_mode'].split(',')
293 if len(view_modes) > 1:
295 raise ValueError('Non-db action dictionaries should provide '
296 'either multiple view modes or a single view '
297 'mode and an optional view id.\n\n Got view '
298 'modes %r and view id %r for action %r' % (
299 view_modes, view_id, action))
300 action['views'] = [(False, mode) for mode in view_modes]
302 action['views'] = [(view_id, view_modes[0])]
305 def fix_view_modes(action):
306 """ For historical reasons, OpenERP has weird dealings in relation to
307 view_mode and the view_type attribute (on window actions):
309 * one of the view modes is ``tree``, which stands for both list views
311 * the choice is made by checking ``view_type``, which is either
312 ``form`` for a list view or ``tree`` for an actual tree view
314 This methods simply folds the view_type into view_mode by adding a
315 new view mode ``list`` which is the result of the ``tree`` view_mode
316 in conjunction with the ``form`` view_type.
318 TODO: this should go into the doc, some kind of "peculiarities" section
320 :param dict action: an action descriptor
321 :returns: nothing, the action is modified in place
323 if 'views' not in action:
324 generate_views(action)
326 if action.pop('view_type') != 'form':
330 [id, mode if mode != 'tree' else 'list']
331 for id, mode in action['views']
336 class Menu(openerpweb.Controller):
337 _cp_path = "/base/menu"
339 @openerpweb.jsonrequest
341 return {'data': self.do_load(req)}
343 def do_load(self, req):
344 """ Loads all menu items (all applications and their sub-menus).
346 :param req: A request object, with an OpenERP session attribute
347 :type req: < session -> OpenERPSession >
348 :return: the menu root
349 :rtype: dict('children': menu_nodes)
351 Menus = req.session.model('ir.ui.menu')
352 # menus are loaded fully unlike a regular tree view, cause there are
353 # less than 512 items
354 context = req.session.eval_context(req.context)
355 menu_ids = Menus.search([], 0, False, False, context)
356 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
357 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
358 menu_items.append(menu_root)
360 # make a tree using parent_id
361 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
362 for menu_item in menu_items:
363 if menu_item['parent_id']:
364 parent = menu_item['parent_id'][0]
367 if parent in menu_items_map:
368 menu_items_map[parent].setdefault(
369 'children', []).append(menu_item)
371 # sort by sequence a tree using parent_id
372 for menu_item in menu_items:
373 menu_item.setdefault('children', []).sort(
374 key=lambda x:x["sequence"])
378 @openerpweb.jsonrequest
379 def action(self, req, menu_id):
380 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
381 [('ir.ui.menu', menu_id)], False,
382 req.session.eval_context(req.context))
383 return {"action": actions}
385 class DataSet(openerpweb.Controller):
386 _cp_path = "/base/dataset"
388 @openerpweb.jsonrequest
389 def fields(self, req, model):
390 return {'fields': req.session.model(model).fields_get(False,
391 req.session.eval_context(req.context))}
393 @openerpweb.jsonrequest
394 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
395 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
396 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
398 """ Performs a search() followed by a read() (if needed) using the
399 provided search criteria
401 :param request: a JSON-RPC request object
402 :type request: openerpweb.JsonRequest
403 :param str model: the name of the model to search on
404 :param fields: a list of the fields to return in the result records
406 :param int offset: from which index should the results start being returned
407 :param int limit: the maximum number of records to return
408 :param list domain: the search domain for the query
409 :param list sort: sorting directives
410 :returns: A structure (dict) with two keys: ids (all the ids matching
411 the (domain, context) pair) and records (paginated records
412 matching fields selection set)
415 Model = request.session.model(model)
417 context, domain = eval_context_and_domain(
418 request.session, request.context, domain)
420 ids = Model.search(domain, 0, False, sort or False, context)
421 # need to fill the dataset with all ids for the (domain, context) pair,
422 # so search un-paginated and paginate manually before reading
423 paginated_ids = ids[offset:(offset + limit if limit else None)]
424 if fields and fields == ['id']:
425 # shortcut read if we only want the ids
428 'records': map(lambda id: {'id': id}, paginated_ids)
431 records = Model.read(paginated_ids, fields or False, context)
432 records.sort(key=lambda obj: ids.index(obj['id']))
439 @openerpweb.jsonrequest
440 def read(self, request, model, ids, fields=False):
441 return self.do_search_read(request, model, ids, fields)
443 @openerpweb.jsonrequest
444 def get(self, request, model, ids, fields=False):
445 return self.do_get(request, model, ids, fields)
447 def do_get(self, request, model, ids, fields=False):
448 """ Fetches and returns the records of the model ``model`` whose ids
451 The results are in the same order as the inputs, but elements may be
452 missing (if there is no record left for the id)
454 :param request: the JSON-RPC2 request object
455 :type request: openerpweb.JsonRequest
456 :param model: the model to read from
458 :param ids: a list of identifiers
460 :param fields: a list of fields to fetch, ``False`` or empty to fetch
461 all fields in the model
462 :type fields: list | False
463 :returns: a list of records, in the same order as the list of ids
466 Model = request.session.model(model)
467 records = Model.read(ids, fields, request.session.eval_context(request.context))
469 record_map = dict((record['id'], record) for record in records)
471 return [record_map[id] for id in ids if record_map.get(id)]
473 @openerpweb.jsonrequest
474 def load(self, req, model, id, fields):
475 m = req.session.model(model)
477 r = m.read([id], False, req.session.eval_context(req.context))
480 return {'value': value}
482 @openerpweb.jsonrequest
483 def create(self, req, model, data):
484 m = req.session.model(model)
485 r = m.create(data, req.session.eval_context(req.context))
488 @openerpweb.jsonrequest
489 def save(self, req, model, id, data):
490 m = req.session.model(model)
491 r = m.write([id], data, req.session.eval_context(req.context))
494 @openerpweb.jsonrequest
495 def unlink(self, request, model, ids=()):
496 Model = request.session.model(model)
497 return Model.unlink(ids, request.session.eval_context(request.context))
499 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
500 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
501 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
502 c, d = eval_context_and_domain(req.session, context, domain)
503 if domain_id and len(args) - 1 >= domain_id:
505 if context_id and len(args) - 1 >= context_id:
508 return getattr(req.session.model(model), method)(*args)
510 @openerpweb.jsonrequest
511 def call(self, req, model, method, args, domain_id=None, context_id=None):
512 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
514 @openerpweb.jsonrequest
515 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
516 action = self.call_common(req, model, method, args, domain_id, context_id)
517 if isinstance(action, dict) and action.get('type') != '':
518 return {'result': clean_action(action, req.session)}
519 return {'result': False}
521 @openerpweb.jsonrequest
522 def exec_workflow(self, req, model, id, signal):
523 r = req.session.exec_workflow(model, id, signal)
526 @openerpweb.jsonrequest
527 def default_get(self, req, model, fields):
528 m = req.session.model(model)
529 r = m.default_get(fields, req.session.eval_context(req.context))
532 @openerpweb.jsonrequest
533 def name_search(self, req, model, search_str, domain=[], context={}):
534 m = req.session.model(model)
535 r = m.name_search(search_str+'%', domain, '=ilike', context)
538 class DataGroup(openerpweb.Controller):
539 _cp_path = "/base/group"
540 @openerpweb.jsonrequest
541 def read(self, request, model, fields, group_by_fields, domain=None):
542 Model = request.session.model(model)
543 context, domain = eval_context_and_domain(request.session, request.context, domain)
545 return Model.read_group(
546 domain or [], fields, group_by_fields, 0, False,
547 dict(context, group_by=group_by_fields))
549 class View(openerpweb.Controller):
550 _cp_path = "/base/view"
552 def fields_view_get(self, request, model, view_id, view_type,
553 transform=True, toolbar=False, submenu=False):
554 Model = request.session.model(model)
555 context = request.session.eval_context(request.context)
556 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
557 # todo fme?: check that we should pass the evaluated context here
558 self.process_view(request.session, fvg, context, transform)
561 def process_view(self, session, fvg, context, transform):
562 # depending on how it feels, xmlrpclib.ServerProxy can translate
563 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
564 # enjoy unicode strings which can not be trivially converted to
565 # strings, and it blows up during parsing.
567 # So ensure we fix this retardation by converting view xml back to
569 if isinstance(fvg['arch'], unicode):
570 arch = fvg['arch'].encode('utf-8')
575 evaluation_context = session.evaluation_context(context or {})
576 xml = self.transform_view(arch, session, evaluation_context)
578 xml = ElementTree.fromstring(arch)
579 fvg['arch'] = Xml2Json.convert_element(xml)
580 for field in fvg['fields'].values():
581 if field.has_key('views') and field['views']:
582 for view in field["views"].values():
583 self.process_view(session, view, None, transform)
585 @openerpweb.jsonrequest
586 def add_custom(self, request, view_id, arch):
587 CustomView = request.session.model('ir.ui.view.custom')
589 'user_id': request.session._uid,
592 }, request.session.eval_context(request.context))
593 return {'result': True}
595 @openerpweb.jsonrequest
596 def undo_custom(self, request, view_id, reset=False):
597 CustomView = request.session.model('ir.ui.view.custom')
598 context = request.session.eval_context(request.context)
599 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
600 0, False, False, context)
603 CustomView.unlink(vcustom, context)
605 CustomView.unlink([vcustom[0]], context)
606 return {'result': True}
607 return {'result': False}
609 def transform_view(self, view_string, session, context=None):
610 # transform nodes on the fly via iterparse, instead of
611 # doing it statically on the parsing result
612 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
614 for event, elem in parser:
618 self.parse_domains_and_contexts(elem, session)
621 def parse_domain(self, elem, attr_name, session):
622 """ Parses an attribute of the provided name as a domain, transforms it
623 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
625 :param elem: the node being parsed
626 :type param: xml.etree.ElementTree.Element
627 :param str attr_name: the name of the attribute which should be parsed
628 :param session: Current OpenERP session
629 :type session: openerpweb.openerpweb.OpenERPSession
631 domain = elem.get(attr_name, '').strip()
636 openerpweb.ast.literal_eval(
641 openerpweb.nonliterals.Domain(session, domain))
643 def parse_domains_and_contexts(self, elem, session):
644 """ Converts domains and contexts from the view into Python objects,
645 either literals if they can be parsed by literal_eval or a special
646 placeholder object if the domain or context refers to free variables.
648 :param elem: the current node being parsed
649 :type param: xml.etree.ElementTree.Element
650 :param session: OpenERP session object, used to store and retrieve
652 :type session: openerpweb.openerpweb.OpenERPSession
654 self.parse_domain(elem, 'domain', session)
655 self.parse_domain(elem, 'filter_domain', session)
656 for el in ['context', 'default_get']:
657 context_string = elem.get(el, '').strip()
661 openerpweb.ast.literal_eval(context_string))
664 openerpweb.nonliterals.Context(
665 session, context_string))
667 class FormView(View):
668 _cp_path = "/base/formview"
670 @openerpweb.jsonrequest
671 def load(self, req, model, view_id, toolbar=False):
672 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
673 return {'fields_view': fields_view}
675 class ListView(View):
676 _cp_path = "/base/listview"
678 @openerpweb.jsonrequest
679 def load(self, req, model, view_id, toolbar=False):
680 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
681 return {'fields_view': fields_view}
683 def process_colors(self, view, row, context):
684 colors = view['arch']['attrs'].get('colors')
691 for pair in colors.split(';')
692 if eval(pair.split(':')[1], dict(context, **row))
697 elif len(color) == 1:
701 class SearchView(View):
702 _cp_path = "/base/searchview"
704 @openerpweb.jsonrequest
705 def load(self, req, model, view_id):
706 fields_view = self.fields_view_get(req, model, view_id, 'search')
707 return {'fields_view': fields_view}
709 @openerpweb.jsonrequest
710 def fields_get(self, req, model):
711 Model = req.session.model(model)
712 fields = Model.fields_get(False, req.session.eval_context(req.context))
713 return {'fields': fields}
715 class Binary(openerpweb.Controller):
716 _cp_path = "/base/binary"
718 @openerpweb.httprequest
719 def image(self, request, session_id, model, id, field, **kw):
720 cherrypy.response.headers['Content-Type'] = 'image/png'
721 Model = request.session.model(model)
722 context = request.session.eval_context(request.context)
725 res = Model.default_get([field], context).get(field, '')
727 res = Model.read([int(id)], [field], context)[0].get(field, '')
728 return base64.decodestring(res)
729 except: # TODO: what's the exception here?
730 return self.placeholder()
731 def placeholder(self):
732 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
734 @openerpweb.httprequest
735 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
736 Model = request.session.model(model)
737 context = request.session.eval_context(request.context)
738 res = Model.read([int(id)], [field, fieldname], context)[0]
739 filecontent = res.get(field, '')
741 raise cherrypy.NotFound
743 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
744 filename = '%s_%s' % (model.replace('.', '_'), id)
746 filename = res.get(fieldname, '') or filename
747 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
748 return base64.decodestring(filecontent)
750 @openerpweb.httprequest
751 def upload(self, request, session_id, callback, ufile=None):
752 cherrypy.response.timeout = 500
754 for key, val in cherrypy.request.headers.iteritems():
755 headers[key.lower()] = val
756 size = int(headers.get('content-length', 0))
757 # TODO: might be useful to have a configuration flag for max-length file uploads
759 out = """<script language="javascript" type="text/javascript">
760 var win = window.top.window,
762 if (typeof(callback) === 'function') {
763 callback.apply(this, %s);
765 win.jQuery('#oe_notification', win.document).notify('create', {
766 title: "Ajax File Upload",
767 text: "Could not find callback"
771 data = ufile.file.read()
772 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
774 args = [False, e.message]
775 return out % (simplejson.dumps(callback), simplejson.dumps(args))
777 @openerpweb.httprequest
778 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
779 cherrypy.response.timeout = 500
780 context = request.session.eval_context(request.context)
781 Model = request.session.model('ir.attachment')
783 out = """<script language="javascript" type="text/javascript">
784 var win = window.top.window,
786 if (typeof(callback) === 'function') {
787 callback.call(this, %s);
790 attachment_id = Model.create({
791 'name': ufile.filename,
792 'datas': base64.encodestring(ufile.file.read()),
797 'filename': ufile.filename,
801 args = { 'error': e.message }
802 return out % (simplejson.dumps(callback), simplejson.dumps(args))
804 class Action(openerpweb.Controller):
805 _cp_path = "/base/action"
807 @openerpweb.jsonrequest
808 def load(self, req, action_id):
809 Actions = req.session.model('ir.actions.actions')
811 context = req.session.eval_context(req.context)
812 action_type = Actions.read([action_id], ['type'], context)
814 action = req.session.model(action_type[0]['type']).read([action_id], False,
817 value = clean_action(action[0], req.session)
818 return {'result': value}
820 class TreeView(View):
821 _cp_path = "/base/treeview"
823 @openerpweb.jsonrequest
824 def load(self, req, model, view_id, toolbar=False):
825 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)