1 # -*- coding: utf-8 -*-
2 import base64, glob, os, re
3 from xml.etree import ElementTree
4 from cStringIO import StringIO
10 import openerpweb.nonliterals
14 # Should move to openerpweb.Xml2Json
17 # Simple and straightforward XML-to-JSON converter in Python
20 # URL: http://code.google.com/p/xml2json-direct/
22 def convert_to_json(s):
23 return simplejson.dumps(
24 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
27 def convert_to_structure(s):
28 root = ElementTree.fromstring(s)
29 return Xml2Json.convert_element(root)
32 def convert_element(el, skip_whitespaces=True):
35 ns, name = el.tag.rsplit("}", 1)
37 res["namespace"] = ns[1:]
41 for k, v in el.items():
44 if el.text and (not skip_whitespaces or el.text.strip() != ''):
47 kids.append(Xml2Json.convert_element(kid))
48 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
50 res["children"] = kids
53 #----------------------------------------------------------
54 # OpenERP Web base Controllers
55 #----------------------------------------------------------
57 class Database(openerpweb.Controller):
58 _cp_path = "/base/database"
60 @openerpweb.jsonrequest
61 def get_databases_list(self, req):
62 proxy = req.session.proxy("db")
64 h = req.httprequest.headers['Host'].split(':')[0]
66 r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
68 dbs = [i for i in dbs if re.match(r,i)]
69 return {"db_list": dbs}
71 class Session(openerpweb.Controller):
72 _cp_path = "/base/session"
74 def manifest_glob(self, addons, key):
77 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
80 resource_path[len(openerpweb.path_addons):]
81 for pattern in globlist
82 for resource_path in glob.glob(os.path.join(
83 openerpweb.path_addons, addon, pattern))
87 def concat_files(self, file_list):
88 """ Concatenate file content
89 return (concat,timestamp)
90 concat: concatenation of file content
91 timestamp: max(os.path.getmtime of file_list)
93 root = openerpweb.path_root
97 fname = os.path.join(root, i)
98 ftime = os.path.getmtime(fname)
99 if ftime > files_timestamp:
100 files_timestamp = ftime
101 files_content = open(fname).read()
102 files_concat = "".join(files_content)
105 @openerpweb.jsonrequest
106 def login(self, req, db, login, password):
107 req.session.login(db, login, password)
110 "session_id": req.session_id,
111 "uid": req.session._uid,
114 @openerpweb.jsonrequest
115 def sc_list(self, req):
116 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
117 req.session.eval_context(req.context))
119 @openerpweb.jsonrequest
120 def modules(self, req):
121 return {"modules": [name
122 for name, manifest in openerpweb.addons_manifest.iteritems()
123 if manifest.get('active', True)]}
125 @openerpweb.jsonrequest
126 def csslist(self, req, mods='base'):
127 return {'files': self.manifest_glob(mods.split(','), 'css')}
129 @openerpweb.jsonrequest
130 def jslist(self, req, mods='base'):
131 return {'files': self.manifest_glob(mods.split(','), 'js')}
133 def css(self, req, mods='base'):
134 files = self.manifest_glob(mods.split(','), 'css')
135 concat = self.concat_files(files)[0]
136 # TODO request set the Date of last modif and Etag
140 def js(self, req, mods='base'):
141 files = self.manifest_glob(mods.split(','), 'js')
142 concat = self.concat_files(files)[0]
143 # TODO request set the Date of last modif and Etag
147 @openerpweb.jsonrequest
148 def eval_domain_and_context(self, req, contexts, domains,
150 """ Evaluates sequences of domains and contexts, composing them into
151 a single context, domain or group_by sequence.
153 :param list contexts: list of contexts to merge together. Contexts are
154 evaluated in sequence, all previous contexts
155 are part of their own evaluation context
156 (starting at the session context).
157 :param list domains: list of domains to merge together. Domains are
158 evaluated in sequence and appended to one another
159 (implicit AND), their evaluation domain is the
160 result of merging all contexts.
161 :param list group_by_seq: list of domains (which may be in a different
162 order than the ``contexts`` parameter),
163 evaluated in sequence, their ``'group_by'``
164 key is extracted if they have one.
169 the global context created by merging all of
173 the concatenation of all domains
176 a list of fields to group by, potentially empty (in which case
177 no group by should be performed)
179 context, domain = eval_context_and_domain(req.session,
180 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
181 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
183 group_by_sequence = []
184 for candidate in (group_by_seq or []):
185 ctx = req.session.eval_context(candidate, context)
186 group_by = ctx.get('group_by')
189 elif isinstance(group_by, basestring):
190 group_by_sequence.append(group_by)
192 group_by_sequence.extend(group_by)
197 'group_by': group_by_sequence
200 @openerpweb.jsonrequest
201 def save_session_action(self, req, the_action):
203 This method store an action object in the session object and returns an integer
204 identifying that action. The method get_session_action() can be used to get
207 :param the_action: The action to save in the session.
208 :type the_action: anything
209 :return: A key identifying the saved action.
212 saved_actions = cherrypy.session.get('saved_actions')
213 if not saved_actions:
214 saved_actions = {"next":0, "actions":{}}
215 cherrypy.session['saved_actions'] = saved_actions
216 # we don't allow more than 10 stored actions
217 if len(saved_actions["actions"]) >= 10:
218 del saved_actions["actions"][min(saved_actions["actions"].keys())]
219 key = saved_actions["next"]
220 saved_actions["actions"][key] = the_action
221 saved_actions["next"] = key + 1
224 @openerpweb.jsonrequest
225 def get_session_action(self, req, key):
227 Gets back a previously saved action. This method can return None if the action
228 was saved since too much time (this case should be handled in a smart way).
230 :param key: The key given by save_session_action()
232 :return: The saved action or None.
235 saved_actions = cherrypy.session.get('saved_actions')
236 if not saved_actions:
238 return saved_actions["actions"].get(key)
240 def eval_context_and_domain(session, context, domain=None):
241 e_context = session.eval_context(context)
242 # should we give the evaluated context as an evaluation context to the domain?
243 e_domain = session.eval_domain(domain or [])
245 return e_context, e_domain
247 def load_actions_from_ir_values(req, key, key2, models, meta, context):
248 Values = req.session.model('ir.values')
249 actions = Values.get(key, key2, models, meta, context)
251 return [(id, name, clean_action(action, req.session))
252 for id, name, action in actions]
254 def clean_action(action, session):
255 if action['type'] != 'ir.actions.act_window':
257 # values come from the server, we can just eval them
258 if isinstance(action.get('context', None), basestring):
259 action['context'] = eval(
261 session.evaluation_context()) or {}
263 if isinstance(action.get('domain', None), basestring):
264 action['domain'] = eval(
266 session.evaluation_context(
267 action['context'])) or []
268 if 'flags' not in action:
269 # Set empty flags dictionary for web client.
270 action['flags'] = dict()
271 return fix_view_modes(action)
273 def generate_views(action):
275 While the server generates a sequence called "views" computing dependencies
276 between a bunch of stuff for views coming directly from the database
277 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
278 to return custom view dictionaries generated on the fly.
280 In that case, there is no ``views`` key available on the action.
282 Since the web client relies on ``action['views']``, generate it here from
283 ``view_mode`` and ``view_id``.
285 Currently handles two different cases:
287 * no view_id, multiple view_mode
288 * single view_id, single view_mode
290 :param dict action: action descriptor dictionary to generate a views key for
292 view_id = action.get('view_id', False)
293 if isinstance(view_id, (list, tuple)):
296 # providing at least one view mode is a requirement, not an option
297 view_modes = action['view_mode'].split(',')
299 if len(view_modes) > 1:
301 raise ValueError('Non-db action dictionaries should provide '
302 'either multiple view modes or a single view '
303 'mode and an optional view id.\n\n Got view '
304 'modes %r and view id %r for action %r' % (
305 view_modes, view_id, action))
306 action['views'] = [(False, mode) for mode in view_modes]
308 action['views'] = [(view_id, view_modes[0])]
310 def fix_view_modes(action):
311 """ For historical reasons, OpenERP has weird dealings in relation to
312 view_mode and the view_type attribute (on window actions):
314 * one of the view modes is ``tree``, which stands for both list views
316 * the choice is made by checking ``view_type``, which is either
317 ``form`` for a list view or ``tree`` for an actual tree view
319 This methods simply folds the view_type into view_mode by adding a
320 new view mode ``list`` which is the result of the ``tree`` view_mode
321 in conjunction with the ``form`` view_type.
323 TODO: this should go into the doc, some kind of "peculiarities" section
325 :param dict action: an action descriptor
326 :returns: nothing, the action is modified in place
328 if 'views' not in action:
329 generate_views(action)
331 if action.pop('view_type') != 'form':
335 [id, mode if mode != 'tree' else 'list']
336 for id, mode in action['views']
341 class Menu(openerpweb.Controller):
342 _cp_path = "/base/menu"
344 @openerpweb.jsonrequest
346 return {'data': self.do_load(req)}
348 def do_load(self, req):
349 """ Loads all menu items (all applications and their sub-menus).
351 :param req: A request object, with an OpenERP session attribute
352 :type req: < session -> OpenERPSession >
353 :return: the menu root
354 :rtype: dict('children': menu_nodes)
356 Menus = req.session.model('ir.ui.menu')
357 # menus are loaded fully unlike a regular tree view, cause there are
358 # less than 512 items
359 context = req.session.eval_context(req.context)
360 menu_ids = Menus.search([], 0, False, False, context)
361 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
362 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
363 menu_items.append(menu_root)
365 # make a tree using parent_id
366 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
367 for menu_item in menu_items:
368 if menu_item['parent_id']:
369 parent = menu_item['parent_id'][0]
372 if parent in menu_items_map:
373 menu_items_map[parent].setdefault(
374 'children', []).append(menu_item)
376 # sort by sequence a tree using parent_id
377 for menu_item in menu_items:
378 menu_item.setdefault('children', []).sort(
379 key=lambda x:x["sequence"])
383 @openerpweb.jsonrequest
384 def action(self, req, menu_id):
385 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
386 [('ir.ui.menu', menu_id)], False,
387 req.session.eval_context(req.context))
388 return {"action": actions}
390 class DataSet(openerpweb.Controller):
391 _cp_path = "/base/dataset"
393 @openerpweb.jsonrequest
394 def fields(self, req, model):
395 return {'fields': req.session.model(model).fields_get(False,
396 req.session.eval_context(req.context))}
398 @openerpweb.jsonrequest
399 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
400 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
401 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
403 """ Performs a search() followed by a read() (if needed) using the
404 provided search criteria
406 :param request: a JSON-RPC request object
407 :type request: openerpweb.JsonRequest
408 :param str model: the name of the model to search on
409 :param fields: a list of the fields to return in the result records
411 :param int offset: from which index should the results start being returned
412 :param int limit: the maximum number of records to return
413 :param list domain: the search domain for the query
414 :param list sort: sorting directives
415 :returns: A structure (dict) with two keys: ids (all the ids matching
416 the (domain, context) pair) and records (paginated records
417 matching fields selection set)
420 Model = request.session.model(model)
421 context, domain = eval_context_and_domain(
422 request.session, request.context, domain)
424 ids = Model.search(domain, 0, False, sort or False, context)
425 # need to fill the dataset with all ids for the (domain, context) pair,
426 # so search un-paginated and paginate manually before reading
427 paginated_ids = ids[offset:(offset + limit if limit else None)]
428 if fields and fields == ['id']:
429 # shortcut read if we only want the ids
432 'records': map(lambda id: {'id': id}, paginated_ids)
435 records = Model.read(paginated_ids, fields or False, context)
436 records.sort(key=lambda obj: ids.index(obj['id']))
443 @openerpweb.jsonrequest
444 def get(self, request, model, ids, fields=False):
445 return self.do_get(request, model, ids, fields)
446 def do_get(self, request, model, ids, fields=False):
447 """ Fetches and returns the records of the model ``model`` whose ids
450 The results are in the same order as the inputs, but elements may be
451 missing (if there is no record left for the id)
453 :param request: the JSON-RPC2 request object
454 :type request: openerpweb.JsonRequest
455 :param model: the model to read from
457 :param ids: a list of identifiers
459 :param fields: a list of fields to fetch, ``False`` or empty to fetch
460 all fields in the model
461 :type fields: list | False
462 :returns: a list of records, in the same order as the list of ids
465 Model = request.session.model(model)
466 records = Model.read(ids, fields, request.session.eval_context(request.context))
468 record_map = dict((record['id'], record) for record in records)
470 return [record_map[id] for id in ids if record_map.get(id)]
472 @openerpweb.jsonrequest
473 def load(self, req, model, id, fields):
474 m = req.session.model(model)
476 r = m.read([id], False, req.session.eval_context(req.context))
479 return {'value': value}
481 @openerpweb.jsonrequest
482 def create(self, req, model, data):
483 m = req.session.model(model)
484 r = m.create(data, req.session.eval_context(req.context))
487 @openerpweb.jsonrequest
488 def save(self, req, model, id, data):
489 m = req.session.model(model)
490 r = m.write([id], data, req.session.eval_context(req.context))
493 @openerpweb.jsonrequest
494 def unlink(self, request, model, ids=()):
495 Model = request.session.model(model)
496 return Model.unlink(ids, request.session.eval_context(request.context))
498 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
499 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
500 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
501 c, d = eval_context_and_domain(req.session, context, domain)
502 if domain_id and len(args) - 1 >= domain_id:
504 if context_id and len(args) - 1 >= context_id:
507 return getattr(req.session.model(model), method)(*args)
509 @openerpweb.jsonrequest
510 def call(self, req, model, method, args, domain_id=None, context_id=None):
511 return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
513 @openerpweb.jsonrequest
514 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
515 action = self.call_common(req, model, method, args, domain_id, context_id)
516 if isinstance(action, dict) and action.get('type') != '':
517 return {'result': clean_action(action, req.session)}
518 return {'result': False}
520 @openerpweb.jsonrequest
521 def exec_workflow(self, req, model, id, signal):
522 r = req.session.exec_workflow(model, id, signal)
525 @openerpweb.jsonrequest
526 def default_get(self, req, model, fields):
527 m = req.session.model(model)
528 r = m.default_get(fields, req.session.eval_context(req.context))
531 class DataGroup(openerpweb.Controller):
532 _cp_path = "/base/group"
533 @openerpweb.jsonrequest
534 def read(self, request, model, fields, group_by_fields, domain=None):
535 Model = request.session.model(model)
536 context, domain = eval_context_and_domain(request.session, request.context, domain)
538 return Model.read_group(
539 domain or [], fields, group_by_fields, 0, False,
540 dict(context, group_by=group_by_fields))
542 class View(openerpweb.Controller):
543 _cp_path = "/base/view"
545 def fields_view_get(self, request, model, view_id, view_type,
546 transform=True, toolbar=False, submenu=False):
547 Model = request.session.model(model)
548 context = request.session.eval_context(request.context)
549 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
550 # todo fme?: check that we should pass the evaluated context here
551 self.process_view(request.session, fvg, context, transform)
554 def process_view(self, session, fvg, context, transform):
555 # depending on how it feels, xmlrpclib.ServerProxy can translate
556 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
557 # enjoy unicode strings which can not be trivially converted to
558 # strings, and it blows up during parsing.
560 # So ensure we fix this retardation by converting view xml back to
562 if isinstance(fvg['arch'], unicode):
563 arch = fvg['arch'].encode('utf-8')
568 evaluation_context = session.evaluation_context(context or {})
569 xml = self.transform_view(arch, session, evaluation_context)
571 xml = ElementTree.fromstring(arch)
572 fvg['arch'] = Xml2Json.convert_element(xml)
573 for field in fvg['fields'].values():
574 if field.has_key('views') and field['views']:
575 for view in field["views"].values():
576 self.process_view(session, view, None, transform)
578 @openerpweb.jsonrequest
579 def add_custom(self, request, view_id, arch):
580 CustomView = request.session.model('ir.ui.view.custom')
582 'user_id': request.session._uid,
585 }, request.session.eval_context(request.context))
586 return {'result': True}
588 @openerpweb.jsonrequest
589 def undo_custom(self, request, view_id, reset=False):
590 CustomView = request.session.model('ir.ui.view.custom')
591 context = request.session.eval_context(request.context)
592 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
593 0, False, False, context)
596 CustomView.unlink(vcustom, context)
598 CustomView.unlink([vcustom[0]], context)
599 return {'result': True}
600 return {'result': False}
602 def transform_view(self, view_string, session, context=None):
603 # transform nodes on the fly via iterparse, instead of
604 # doing it statically on the parsing result
605 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
607 for event, elem in parser:
611 self.parse_domains_and_contexts(elem, session)
614 def parse_domain(self, elem, attr_name, session):
615 """ Parses an attribute of the provided name as a domain, transforms it
616 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
618 :param elem: the node being parsed
619 :type param: xml.etree.ElementTree.Element
620 :param str attr_name: the name of the attribute which should be parsed
621 :param session: Current OpenERP session
622 :type session: openerpweb.openerpweb.OpenERPSession
624 domain = elem.get(attr_name, '').strip()
629 openerpweb.ast.literal_eval(
634 openerpweb.nonliterals.Domain(session, domain))
636 def parse_domains_and_contexts(self, elem, session):
637 """ Converts domains and contexts from the view into Python objects,
638 either literals if they can be parsed by literal_eval or a special
639 placeholder object if the domain or context refers to free variables.
641 :param elem: the current node being parsed
642 :type param: xml.etree.ElementTree.Element
643 :param session: OpenERP session object, used to store and retrieve
645 :type session: openerpweb.openerpweb.OpenERPSession
647 self.parse_domain(elem, 'domain', session)
648 self.parse_domain(elem, 'filter_domain', session)
649 for el in ['context', 'default_get']:
650 context_string = elem.get(el, '').strip()
654 openerpweb.ast.literal_eval(context_string))
657 openerpweb.nonliterals.Context(
658 session, context_string))
660 class FormView(View):
661 _cp_path = "/base/formview"
663 @openerpweb.jsonrequest
664 def load(self, req, model, view_id, toolbar=False):
665 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
666 return {'fields_view': fields_view}
668 class ListView(View):
669 _cp_path = "/base/listview"
671 @openerpweb.jsonrequest
672 def load(self, req, model, view_id, toolbar=False):
673 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
674 return {'fields_view': fields_view}
676 def process_colors(self, view, row, context):
677 colors = view['arch']['attrs'].get('colors')
684 for pair in colors.split(';')
685 if eval(pair.split(':')[1], dict(context, **row))
690 elif len(color) == 1:
694 class SearchView(View):
695 _cp_path = "/base/searchview"
697 @openerpweb.jsonrequest
698 def load(self, req, model, view_id):
699 fields_view = self.fields_view_get(req, model, view_id, 'search')
700 return {'fields_view': fields_view}
702 @openerpweb.jsonrequest
703 def fields_get(self, req, model):
704 Model = req.session.model(model)
705 fields = Model.fields_get(False, req.session.eval_context(req.context))
706 return {'fields': fields}
708 class Binary(openerpweb.Controller):
709 _cp_path = "/base/binary"
711 @openerpweb.httprequest
712 def image(self, request, session_id, model, id, field, **kw):
713 cherrypy.response.headers['Content-Type'] = 'image/png'
714 Model = request.session.model(model)
715 context = request.session.eval_context(request.context)
718 res = Model.default_get([field], context).get(field, '')
720 res = Model.read([int(id)], [field], context)[0].get(field, '')
721 return base64.decodestring(res)
722 except: # TODO: what's the exception here?
723 return self.placeholder()
724 def placeholder(self):
725 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
727 @openerpweb.httprequest
728 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
729 Model = request.session.model(model)
730 context = request.session.eval_context(request.context)
731 res = Model.read([int(id)], [field, fieldname], context)[0]
732 filecontent = res.get(field, '')
734 raise cherrypy.NotFound
736 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
737 filename = '%s_%s' % (model.replace('.', '_'), id)
739 filename = res.get(fieldname, '') or filename
740 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
741 return base64.decodestring(filecontent)
743 @openerpweb.httprequest
744 def upload(self, request, session_id, callback, ufile=None):
745 cherrypy.response.timeout = 500
747 for key, val in cherrypy.request.headers.iteritems():
748 headers[key.lower()] = val
749 size = int(headers.get('content-length', 0))
750 # TODO: might be useful to have a configuration flag for max-length file uploads
752 out = """<script language="javascript" type="text/javascript">
753 var win = window.top.window,
755 if (typeof(callback) === 'function') {
756 callback.apply(this, %s);
758 win.jQuery('#oe_notification', win.document).notify('create', {
759 title: "Ajax File Upload",
760 text: "Could not find callback"
764 data = ufile.file.read()
765 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
767 args = [False, e.message]
768 return out % (simplejson.dumps(callback), simplejson.dumps(args))
770 @openerpweb.httprequest
771 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
772 cherrypy.response.timeout = 500
773 context = request.session.eval_context(request.context)
774 Model = request.session.model('ir.attachment')
776 out = """<script language="javascript" type="text/javascript">
777 var win = window.top.window,
779 if (typeof(callback) === 'function') {
780 callback.call(this, %s);
783 attachment_id = Model.create({
784 'name': ufile.filename,
785 'datas': base64.encodestring(ufile.file.read()),
790 'filename': ufile.filename,
794 args = { 'error': e.message }
795 return out % (simplejson.dumps(callback), simplejson.dumps(args))
797 class Action(openerpweb.Controller):
798 _cp_path = "/base/action"
800 @openerpweb.jsonrequest
801 def load(self, req, action_id):
802 Actions = req.session.model('ir.actions.actions')
804 context = req.session.eval_context(req.context)
805 action_type = Actions.read([action_id], ['type'], context)
807 action = req.session.model(action_type[0]['type']).read([action_id], False,
810 value = clean_action(action[0], req.session)
811 return {'result': value}
813 @openerpweb.jsonrequest
814 def run(self, req, action_id):
815 return clean_action(req.session.model('ir.actions.server').run(
816 [action_id], req.session.eval_context(req.context)), req.session)