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 @openerpweb.jsonrequest
241 def check(self, req):
242 req.session.assert_valid()
245 def eval_context_and_domain(session, context, domain=None):
246 e_context = session.eval_context(context)
247 # should we give the evaluated context as an evaluation context to the domain?
248 e_domain = session.eval_domain(domain or [])
250 return e_context, e_domain
252 def load_actions_from_ir_values(req, key, key2, models, meta, context):
253 Values = req.session.model('ir.values')
254 actions = Values.get(key, key2, models, meta, context)
256 return [(id, name, clean_action(action, req.session))
257 for id, name, action in actions]
259 def clean_action(action, session):
260 if action['type'] != 'ir.actions.act_window':
262 # values come from the server, we can just eval them
263 if isinstance(action.get('context', None), basestring):
264 action['context'] = eval(
266 session.evaluation_context()) or {}
268 if isinstance(action.get('domain', None), basestring):
269 action['domain'] = eval(
271 session.evaluation_context(
272 action['context'])) or []
273 if 'flags' not in action:
274 # Set empty flags dictionary for web client.
275 action['flags'] = dict()
276 return fix_view_modes(action)
278 def generate_views(action):
280 While the server generates a sequence called "views" computing dependencies
281 between a bunch of stuff for views coming directly from the database
282 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
283 to return custom view dictionaries generated on the fly.
285 In that case, there is no ``views`` key available on the action.
287 Since the web client relies on ``action['views']``, generate it here from
288 ``view_mode`` and ``view_id``.
290 Currently handles two different cases:
292 * no view_id, multiple view_mode
293 * single view_id, single view_mode
295 :param dict action: action descriptor dictionary to generate a views key for
297 view_id = action.get('view_id', False)
298 if isinstance(view_id, (list, tuple)):
301 # providing at least one view mode is a requirement, not an option
302 view_modes = action['view_mode'].split(',')
304 if len(view_modes) > 1:
306 raise ValueError('Non-db action dictionaries should provide '
307 'either multiple view modes or a single view '
308 'mode and an optional view id.\n\n Got view '
309 'modes %r and view id %r for action %r' % (
310 view_modes, view_id, action))
311 action['views'] = [(False, mode) for mode in view_modes]
313 action['views'] = [(view_id, view_modes[0])]
315 def fix_view_modes(action):
316 """ For historical reasons, OpenERP has weird dealings in relation to
317 view_mode and the view_type attribute (on window actions):
319 * one of the view modes is ``tree``, which stands for both list views
321 * the choice is made by checking ``view_type``, which is either
322 ``form`` for a list view or ``tree`` for an actual tree view
324 This methods simply folds the view_type into view_mode by adding a
325 new view mode ``list`` which is the result of the ``tree`` view_mode
326 in conjunction with the ``form`` view_type.
328 TODO: this should go into the doc, some kind of "peculiarities" section
330 :param dict action: an action descriptor
331 :returns: nothing, the action is modified in place
333 if 'views' not in action:
334 generate_views(action)
336 if action.pop('view_type') != 'form':
340 [id, mode if mode != 'tree' else 'list']
341 for id, mode in action['views']
346 class Menu(openerpweb.Controller):
347 _cp_path = "/base/menu"
349 @openerpweb.jsonrequest
351 return {'data': self.do_load(req)}
353 def do_load(self, req):
354 """ Loads all menu items (all applications and their sub-menus).
356 :param req: A request object, with an OpenERP session attribute
357 :type req: < session -> OpenERPSession >
358 :return: the menu root
359 :rtype: dict('children': menu_nodes)
361 Menus = req.session.model('ir.ui.menu')
362 # menus are loaded fully unlike a regular tree view, cause there are
363 # less than 512 items
364 context = req.session.eval_context(req.context)
365 menu_ids = Menus.search([], 0, False, False, context)
366 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
367 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
368 menu_items.append(menu_root)
370 # make a tree using parent_id
371 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
372 for menu_item in menu_items:
373 if menu_item['parent_id']:
374 parent = menu_item['parent_id'][0]
377 if parent in menu_items_map:
378 menu_items_map[parent].setdefault(
379 'children', []).append(menu_item)
381 # sort by sequence a tree using parent_id
382 for menu_item in menu_items:
383 menu_item.setdefault('children', []).sort(
384 key=lambda x:x["sequence"])
388 @openerpweb.jsonrequest
389 def action(self, req, menu_id):
390 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
391 [('ir.ui.menu', menu_id)], False,
392 req.session.eval_context(req.context))
393 return {"action": actions}
395 class DataSet(openerpweb.Controller):
396 _cp_path = "/base/dataset"
398 @openerpweb.jsonrequest
399 def fields(self, req, model):
400 return {'fields': req.session.model(model).fields_get(False,
401 req.session.eval_context(req.context))}
403 @openerpweb.jsonrequest
404 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
405 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
406 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
408 """ Performs a search() followed by a read() (if needed) using the
409 provided search criteria
411 :param request: a JSON-RPC request object
412 :type request: openerpweb.JsonRequest
413 :param str model: the name of the model to search on
414 :param fields: a list of the fields to return in the result records
416 :param int offset: from which index should the results start being returned
417 :param int limit: the maximum number of records to return
418 :param list domain: the search domain for the query
419 :param list sort: sorting directives
420 :returns: A structure (dict) with two keys: ids (all the ids matching
421 the (domain, context) pair) and records (paginated records
422 matching fields selection set)
425 Model = request.session.model(model)
426 context, domain = eval_context_and_domain(
427 request.session, request.context, domain)
429 ids = Model.search(domain, 0, False, sort or False, context)
430 # need to fill the dataset with all ids for the (domain, context) pair,
431 # so search un-paginated and paginate manually before reading
432 paginated_ids = ids[offset:(offset + limit if limit else None)]
433 if fields and fields == ['id']:
434 # shortcut read if we only want the ids
437 'records': map(lambda id: {'id': id}, paginated_ids)
440 records = Model.read(paginated_ids, fields or False, context)
441 records.sort(key=lambda obj: ids.index(obj['id']))
448 @openerpweb.jsonrequest
449 def get(self, request, model, ids, fields=False):
450 return self.do_get(request, model, ids, fields)
451 def do_get(self, request, model, ids, fields=False):
452 """ Fetches and returns the records of the model ``model`` whose ids
455 The results are in the same order as the inputs, but elements may be
456 missing (if there is no record left for the id)
458 :param request: the JSON-RPC2 request object
459 :type request: openerpweb.JsonRequest
460 :param model: the model to read from
462 :param ids: a list of identifiers
464 :param fields: a list of fields to fetch, ``False`` or empty to fetch
465 all fields in the model
466 :type fields: list | False
467 :returns: a list of records, in the same order as the list of ids
470 Model = request.session.model(model)
471 records = Model.read(ids, fields, request.session.eval_context(request.context))
473 record_map = dict((record['id'], record) for record in records)
475 return [record_map[id] for id in ids if record_map.get(id)]
477 @openerpweb.jsonrequest
478 def load(self, req, model, id, fields):
479 m = req.session.model(model)
481 r = m.read([id], False, req.session.eval_context(req.context))
484 return {'value': value}
486 @openerpweb.jsonrequest
487 def create(self, req, model, data):
488 m = req.session.model(model)
489 r = m.create(data, req.session.eval_context(req.context))
492 @openerpweb.jsonrequest
493 def save(self, req, model, id, data):
494 m = req.session.model(model)
495 r = m.write([id], data, req.session.eval_context(req.context))
498 @openerpweb.jsonrequest
499 def unlink(self, request, model, ids=()):
500 Model = request.session.model(model)
501 return Model.unlink(ids, request.session.eval_context(request.context))
503 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
504 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
505 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
506 c, d = eval_context_and_domain(req.session, context, domain)
507 if domain_id and len(args) - 1 >= domain_id:
509 if context_id and len(args) - 1 >= context_id:
512 return getattr(req.session.model(model), method)(*args)
514 @openerpweb.jsonrequest
515 def call(self, req, model, method, args, domain_id=None, context_id=None):
516 return self.call_common(req, model, method, args, domain_id, context_id)
518 @openerpweb.jsonrequest
519 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
520 action = self.call_common(req, model, method, args, domain_id, context_id)
521 if isinstance(action, dict) and action.get('type') != '':
522 return {'result': clean_action(action, req.session)}
523 return {'result': False}
525 @openerpweb.jsonrequest
526 def exec_workflow(self, req, model, id, signal):
527 r = req.session.exec_workflow(model, id, signal)
530 @openerpweb.jsonrequest
531 def default_get(self, req, model, fields):
532 Model = req.session.model(model)
533 return Model.default_get(fields, req.session.eval_context(req.context))
535 class DataGroup(openerpweb.Controller):
536 _cp_path = "/base/group"
537 @openerpweb.jsonrequest
538 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
539 Model = request.session.model(model)
540 context, domain = eval_context_and_domain(request.session, request.context, domain)
542 return Model.read_group(
543 domain or [], fields, group_by_fields, 0, False,
544 dict(context, group_by=group_by_fields), sort or False)
546 class View(openerpweb.Controller):
547 _cp_path = "/base/view"
549 def fields_view_get(self, request, model, view_id, view_type,
550 transform=True, toolbar=False, submenu=False):
551 Model = request.session.model(model)
552 context = request.session.eval_context(request.context)
553 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
554 # todo fme?: check that we should pass the evaluated context here
555 self.process_view(request.session, fvg, context, transform)
558 def process_view(self, session, fvg, context, transform):
559 # depending on how it feels, xmlrpclib.ServerProxy can translate
560 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
561 # enjoy unicode strings which can not be trivially converted to
562 # strings, and it blows up during parsing.
564 # So ensure we fix this retardation by converting view xml back to
566 if isinstance(fvg['arch'], unicode):
567 arch = fvg['arch'].encode('utf-8')
572 evaluation_context = session.evaluation_context(context or {})
573 xml = self.transform_view(arch, session, evaluation_context)
575 xml = ElementTree.fromstring(arch)
576 fvg['arch'] = Xml2Json.convert_element(xml)
577 for field in fvg['fields'].values():
578 if field.has_key('views') and field['views']:
579 for view in field["views"].values():
580 self.process_view(session, view, None, transform)
582 @openerpweb.jsonrequest
583 def add_custom(self, request, view_id, arch):
584 CustomView = request.session.model('ir.ui.view.custom')
586 'user_id': request.session._uid,
589 }, request.session.eval_context(request.context))
590 return {'result': True}
592 @openerpweb.jsonrequest
593 def undo_custom(self, request, view_id, reset=False):
594 CustomView = request.session.model('ir.ui.view.custom')
595 context = request.session.eval_context(request.context)
596 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
597 0, False, False, context)
600 CustomView.unlink(vcustom, context)
602 CustomView.unlink([vcustom[0]], context)
603 return {'result': True}
604 return {'result': False}
606 def transform_view(self, view_string, session, context=None):
607 # transform nodes on the fly via iterparse, instead of
608 # doing it statically on the parsing result
609 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
611 for event, elem in parser:
615 self.parse_domains_and_contexts(elem, session)
618 def parse_domain(self, elem, attr_name, session):
619 """ Parses an attribute of the provided name as a domain, transforms it
620 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
622 :param elem: the node being parsed
623 :type param: xml.etree.ElementTree.Element
624 :param str attr_name: the name of the attribute which should be parsed
625 :param session: Current OpenERP session
626 :type session: openerpweb.openerpweb.OpenERPSession
628 domain = elem.get(attr_name, '').strip()
633 openerpweb.ast.literal_eval(
638 openerpweb.nonliterals.Domain(session, domain))
640 def parse_domains_and_contexts(self, elem, session):
641 """ Converts domains and contexts from the view into Python objects,
642 either literals if they can be parsed by literal_eval or a special
643 placeholder object if the domain or context refers to free variables.
645 :param elem: the current node being parsed
646 :type param: xml.etree.ElementTree.Element
647 :param session: OpenERP session object, used to store and retrieve
649 :type session: openerpweb.openerpweb.OpenERPSession
651 self.parse_domain(elem, 'domain', session)
652 self.parse_domain(elem, 'filter_domain', session)
653 for el in ['context', 'default_get']:
654 context_string = elem.get(el, '').strip()
658 openerpweb.ast.literal_eval(context_string))
661 openerpweb.nonliterals.Context(
662 session, context_string))
664 class FormView(View):
665 _cp_path = "/base/formview"
667 @openerpweb.jsonrequest
668 def load(self, req, model, view_id, toolbar=False):
669 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
670 return {'fields_view': fields_view}
672 class ListView(View):
673 _cp_path = "/base/listview"
675 @openerpweb.jsonrequest
676 def load(self, req, model, view_id, toolbar=False):
677 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
678 return {'fields_view': fields_view}
680 def process_colors(self, view, row, context):
681 colors = view['arch']['attrs'].get('colors')
688 for pair in colors.split(';')
689 if eval(pair.split(':')[1], dict(context, **row))
694 elif len(color) == 1:
698 class SearchView(View):
699 _cp_path = "/base/searchview"
701 @openerpweb.jsonrequest
702 def load(self, req, model, view_id):
703 fields_view = self.fields_view_get(req, model, view_id, 'search')
704 return {'fields_view': fields_view}
706 @openerpweb.jsonrequest
707 def fields_get(self, req, model):
708 Model = req.session.model(model)
709 fields = Model.fields_get(False, req.session.eval_context(req.context))
710 return {'fields': fields}
712 class Binary(openerpweb.Controller):
713 _cp_path = "/base/binary"
715 @openerpweb.httprequest
716 def image(self, request, session_id, model, id, field, **kw):
717 cherrypy.response.headers['Content-Type'] = 'image/png'
718 Model = request.session.model(model)
719 context = request.session.eval_context(request.context)
722 res = Model.default_get([field], context).get(field, '')
724 res = Model.read([int(id)], [field], context)[0].get(field, '')
725 return base64.decodestring(res)
726 except: # TODO: what's the exception here?
727 return self.placeholder()
728 def placeholder(self):
729 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
731 @openerpweb.httprequest
732 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
733 Model = request.session.model(model)
734 context = request.session.eval_context(request.context)
735 res = Model.read([int(id)], [field, fieldname], context)[0]
736 filecontent = res.get(field, '')
738 raise cherrypy.NotFound
740 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
741 filename = '%s_%s' % (model.replace('.', '_'), id)
743 filename = res.get(fieldname, '') or filename
744 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
745 return base64.decodestring(filecontent)
747 @openerpweb.httprequest
748 def upload(self, request, session_id, callback, ufile=None):
749 cherrypy.response.timeout = 500
751 for key, val in cherrypy.request.headers.iteritems():
752 headers[key.lower()] = val
753 size = int(headers.get('content-length', 0))
754 # TODO: might be useful to have a configuration flag for max-length file uploads
756 out = """<script language="javascript" type="text/javascript">
757 var win = window.top.window,
759 if (typeof(callback) === 'function') {
760 callback.apply(this, %s);
762 win.jQuery('#oe_notification', win.document).notify('create', {
763 title: "Ajax File Upload",
764 text: "Could not find callback"
768 data = ufile.file.read()
769 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
771 args = [False, e.message]
772 return out % (simplejson.dumps(callback), simplejson.dumps(args))
774 @openerpweb.httprequest
775 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
776 cherrypy.response.timeout = 500
777 context = request.session.eval_context(request.context)
778 Model = request.session.model('ir.attachment')
780 out = """<script language="javascript" type="text/javascript">
781 var win = window.top.window,
783 if (typeof(callback) === 'function') {
784 callback.call(this, %s);
787 attachment_id = Model.create({
788 'name': ufile.filename,
789 'datas': base64.encodestring(ufile.file.read()),
794 'filename': ufile.filename,
798 args = { 'error': e.message }
799 return out % (simplejson.dumps(callback), simplejson.dumps(args))
801 class Action(openerpweb.Controller):
802 _cp_path = "/base/action"
804 @openerpweb.jsonrequest
805 def load(self, req, action_id):
806 Actions = req.session.model('ir.actions.actions')
808 context = req.session.eval_context(req.context)
809 action_type = Actions.read([action_id], ['type'], context)
811 action = req.session.model(action_type[0]['type']).read([action_id], False,
814 value = clean_action(action[0], req.session)
815 return {'result': value}
817 @openerpweb.jsonrequest
818 def run(self, req, action_id):
819 return clean_action(req.session.model('ir.actions.server').run(
820 [action_id], req.session.eval_context(req.context)), req.session)