1 # -*- coding: utf-8 -*-
5 from xml.etree import ElementTree
6 from cStringIO import StringIO
12 import openerpweb.nonliterals
16 # Should move to openerpweb.Xml2Json
19 # Simple and straightforward XML-to-JSON converter in Python
22 # URL: http://code.google.com/p/xml2json-direct/
24 def convert_to_json(s):
25 return simplejson.dumps(
26 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
29 def convert_to_structure(s):
30 root = ElementTree.fromstring(s)
31 return Xml2Json.convert_element(root)
34 def convert_element(el, skip_whitespaces=True):
37 ns, name = el.tag.rsplit("}", 1)
39 res["namespace"] = ns[1:]
43 for k, v in el.items():
46 if el.text and (not skip_whitespaces or el.text.strip() != ''):
49 kids.append(Xml2Json.convert_element(kid))
50 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
52 res["children"] = kids
55 #----------------------------------------------------------
56 # OpenERP Web base Controllers
57 #----------------------------------------------------------
59 class Session(openerpweb.Controller):
60 _cp_path = "/base/session"
62 def manifest_glob(self, addons, key):
65 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
68 resource_path[len(openerpweb.path_addons):]
69 for pattern in globlist
70 for resource_path in glob.glob(os.path.join(
71 openerpweb.path_addons, addon, pattern))
75 def concat_files(self, file_list):
76 """ Concatenate file content
77 return (concat,timestamp)
78 concat: concatenation of file content
79 timestamp: max(os.path.getmtime of file_list)
81 root = openerpweb.path_root
85 fname = os.path.join(root, i)
86 ftime = os.path.getmtime(fname)
87 if ftime > files_timestamp:
88 files_timestamp = ftime
89 files_content = open(fname).read()
90 files_concat = "".join(files_content)
93 @openerpweb.jsonrequest
94 def login(self, req, db, login, password):
95 req.session.login(db, login, password)
98 "session_id": req.session_id,
99 "uid": req.session._uid,
102 @openerpweb.jsonrequest
103 def sc_list(self, req):
104 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu", {})
106 @openerpweb.jsonrequest
107 def get_databases_list(self, req):
108 proxy = req.session.proxy("db")
111 return {"db_list": dbs}
113 @openerpweb.jsonrequest
114 def modules(self, req):
115 return {"modules": [name
116 for name, manifest in openerpweb.addons_manifest.iteritems()
117 if manifest.get('active', True)]}
119 @openerpweb.jsonrequest
120 def csslist(self, req, mods='base'):
121 return {'files': self.manifest_glob(mods.split(','), 'css')}
123 @openerpweb.jsonrequest
124 def jslist(self, req, mods='base'):
125 return {'files': self.manifest_glob(mods.split(','), 'js')}
127 def css(self, req, mods='base,base_hello'):
128 files = self.manifest_glob(mods.split(','), 'css')
129 concat = self.concat_files(files)[0]
130 # TODO request set the Date of last modif and Etag
134 def js(self, req, mods='base,base_hello'):
135 files = self.manifest_glob(mods.split(','), 'js')
136 concat = self.concat_files(files)[0]
137 # TODO request set the Date of last modif and Etag
141 @openerpweb.jsonrequest
142 def eval_domain_and_context(self, req, contexts, domains,
144 """ Evaluates sequences of domains and contexts, composing them into
145 a single context, domain or group_by sequence.
147 :param list contexts: list of contexts to merge together. Contexts are
148 evaluated in sequence, all previous contexts
149 are part of their own evaluation context
150 (starting at the session context).
151 :param list domains: list of domains to merge together. Domains are
152 evaluated in sequence and appended to one another
153 (implicit AND), their evaluation domain is the
154 result of merging all contexts.
155 :param list group_by_seq: list of domains (which may be in a different
156 order than the ``contexts`` parameter),
157 evaluated in sequence, their ``'group_by'``
158 key is extracted if they have one.
163 the global context created by merging all of
167 the concatenation of all domains
170 a list of fields to group by, potentially empty (in which case
171 no group by should be performed)
173 context = req.session.eval_contexts(contexts)
174 domain = req.session.eval_domains(domains, context)
176 group_by_sequence = []
177 for candidate in (group_by_seq or []):
178 ctx = req.session.eval_context(candidate, context)
179 group_by = ctx.get('group_by')
182 elif isinstance(group_by, basestring):
183 group_by_sequence.append(group_by)
185 group_by_sequence.extend(group_by)
190 'group_by': group_by_sequence
193 @openerpweb.jsonrequest
194 def save_session_action(self, req, the_action):
196 This method store an action object in the session object and returns an integer
197 identifying that action. The method get_session_action() can be used to get
200 :param the_action: The action to save in the session.
201 :type the_action: anything
202 :return: A key identifying the saved action.
205 saved_actions = cherrypy.session.get('saved_actions')
206 if not saved_actions:
207 saved_actions = {"next":0, "actions":{}}
208 cherrypy.session['saved_actions'] = saved_actions
209 # we don't allow more than 10 stored actions
210 if len(saved_actions["actions"]) >= 10:
211 del saved_actions["actions"][min(saved_actions["actions"].keys())]
212 key = saved_actions["next"]
213 saved_actions["actions"][key] = the_action
214 saved_actions["next"] = key + 1
217 @openerpweb.jsonrequest
218 def get_session_action(self, req, key):
220 Gets back a previously saved action. This method can return None if the action
221 was saved since too much time (this case should be handled in a smart way).
223 :param key: The key given by save_session_action()
225 :return: The saved action or None.
228 saved_actions = cherrypy.session.get('saved_actions')
229 if not saved_actions:
231 return saved_actions["actions"].get(key)
234 def load_actions_from_ir_values(req, key, key2, models, meta, context):
235 Values = req.session.model('ir.values')
236 actions = Values.get(key, key2, models, meta, context)
238 for _, _, action in actions:
239 clean_action(action, req.session)
243 def clean_action(action, session):
244 # values come from the server, we can just eval them
245 if isinstance(action['context'], basestring):
246 action['context'] = eval(
248 session.evaluation_context()) or {}
250 if isinstance(action['domain'], basestring):
251 action['domain'] = eval(
253 session.evaluation_context(
254 action['context'])) or []
255 if not action.has_key('flags'):
256 # Set empty flags dictionary for web client.
257 action['flags'] = dict()
258 return fix_view_modes(action)
260 def fix_view_modes(action):
261 """ For historical reasons, OpenERP has weird dealings in relation to
262 view_mode and the view_type attribute (on window actions):
264 * one of the view modes is ``tree``, which stands for both list views
266 * the choice is made by checking ``view_type``, which is either
267 ``form`` for a list view or ``tree`` for an actual tree view
269 This methods simply folds the view_type into view_mode by adding a
270 new view mode ``list`` which is the result of the ``tree`` view_mode
271 in conjunction with the ``form`` view_type.
273 TODO: this should go into the doc, some kind of "peculiarities" section
275 :param dict action: an action descriptor
276 :returns: nothing, the action is modified in place
278 if action.pop('view_type') != 'form':
281 action['view_mode'] = ','.join(
282 mode if mode != 'tree' else 'list'
283 for mode in action['view_mode'].split(','))
285 [id, mode if mode != 'tree' else 'list']
286 for id, mode in action['views']
290 class Menu(openerpweb.Controller):
291 _cp_path = "/base/menu"
293 @openerpweb.jsonrequest
295 return {'data': self.do_load(req)}
297 def do_load(self, req):
298 """ Loads all menu items (all applications and their sub-menus).
300 :param req: A request object, with an OpenERP session attribute
301 :type req: < session -> OpenERPSession >
302 :return: the menu root
303 :rtype: dict('children': menu_nodes)
305 Menus = req.session.model('ir.ui.menu')
306 # menus are loaded fully unlike a regular tree view, cause there are
307 # less than 512 items
308 menu_ids = Menus.search([])
309 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'])
310 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
311 menu_items.append(menu_root)
313 # make a tree using parent_id
314 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
315 for menu_item in menu_items:
316 if menu_item['parent_id']:
317 parent = menu_item['parent_id'][0]
320 if parent in menu_items_map:
321 menu_items_map[parent].setdefault(
322 'children', []).append(menu_item)
324 # sort by sequence a tree using parent_id
325 for menu_item in menu_items:
326 menu_item.setdefault('children', []).sort(
327 key=lambda x:x["sequence"])
331 @openerpweb.jsonrequest
332 def action(self, req, menu_id):
333 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
334 [('ir.ui.menu', menu_id)], False, {})
336 return {"action": actions}
338 class DataSet(openerpweb.Controller):
339 _cp_path = "/base/dataset"
341 @openerpweb.jsonrequest
342 def fields(self, req, model):
343 return {'fields': req.session.model(model).fields_get()}
345 @openerpweb.jsonrequest
346 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
347 return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
348 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
349 """ Performs a search() followed by a read() (if needed) using the
350 provided search criteria
352 :param request: a JSON-RPC request object
353 :type request: openerpweb.JsonRequest
354 :param str model: the name of the model to search on
355 :param fields: a list of the fields to return in the result records
357 :param int offset: from which index should the results start being returned
358 :param int limit: the maximum number of records to return
359 :param list domain: the search domain for the query
360 :param list sort: sorting directives
361 :returns: a list of result records
364 Model = request.session.model(model)
366 ids = Model.search(domain or [], offset or 0, limit or False,
367 sort or False, request.context)
369 if fields and fields == ['id']:
370 # shortcut read if we only want the ids
371 return map(lambda id: {'id': id}, ids)
373 reads = Model.read(ids, fields or False, request.context)
374 reads.sort(key=lambda obj: ids.index(obj['id']))
377 @openerpweb.jsonrequest
378 def read(self, request, model, ids, fields=False):
379 return self.do_search_read(request, model, ids, fields)
381 @openerpweb.jsonrequest
382 def get(self, request, model, ids, fields=False):
383 return self.do_get(request, model, ids, fields)
384 def do_get(self, request, model, ids, fields=False):
385 """ Fetches and returns the records of the model ``model`` whose ids
388 The results are in the same order as the inputs, but elements may be
389 missing (if there is no record left for the id)
391 :param request: the JSON-RPC2 request object
392 :type request: openerpweb.JsonRequest
393 :param model: the model to read from
395 :param ids: a list of identifiers
397 :param fields: a list of fields to fetch, ``False`` or empty to fetch
398 all fields in the model
399 :type fields: list | False
400 :returns: a list of records, in the same order as the list of ids
403 Model = request.session.model(model)
404 records = Model.read(ids, fields, request.context)
406 record_map = dict((record['id'], record) for record in records)
408 return [record_map[id] for id in ids if record_map.get(id)]
409 @openerpweb.jsonrequest
411 def load(self, req, model, id, fields):
412 m = req.session.model(model)
417 return {'value': value}
419 @openerpweb.jsonrequest
420 def create(self, req, model, data, context={}):
421 m = req.session.model(model)
422 r = m.create(data, context)
425 @openerpweb.jsonrequest
426 def save(self, req, model, id, data, context={}):
427 m = req.session.model(model)
428 r = m.write([id], data, context)
431 @openerpweb.jsonrequest
432 def unlink(self, request, model, ids=[]):
433 Model = request.session.model(model)
434 return Model.unlink(ids)
436 @openerpweb.jsonrequest
437 def call(self, req, model, method, args):
438 m = req.session.model(model)
439 r = getattr(m, method)(*args)
442 @openerpweb.jsonrequest
443 def exec_workflow(self, req, model, id, signal):
444 r = req.session.exec_workflow(model, id, signal)
447 @openerpweb.jsonrequest
448 def default_get(self, req, model, fields, context={}):
449 m = req.session.model(model)
450 r = m.default_get(fields, context)
453 class DataGroup(openerpweb.Controller):
454 _cp_path = "/base/group"
455 @openerpweb.jsonrequest
456 def read(self, request, model, group_by_fields, domain=None):
457 Model = request.session.model(model)
459 return Model.read_group(
460 domain or [], False, group_by_fields, 0, False,
461 dict(request.context, group_by=group_by_fields))
463 class View(openerpweb.Controller):
464 _cp_path = "/base/view"
466 def fields_view_get(self, request, model, view_id, view_type,
467 transform=True, toolbar=False, submenu=False):
468 Model = request.session.model(model)
469 fvg = Model.fields_view_get(view_id, view_type, request.context,
471 self.process_view(request.session, fvg, request.context, transform)
474 def process_view(self, session, fvg, context, transform):
476 evaluation_context = session.evaluation_context(context or {})
477 xml = self.transform_view(fvg['arch'], session, evaluation_context)
479 xml = ElementTree.fromstring(fvg['arch'])
480 fvg['arch'] = Xml2Json.convert_element(xml)
481 for field in fvg['fields'].values():
482 if field.has_key('views') and field['views']:
483 for view in field["views"].values():
484 self.process_view(session, view, None, transform)
486 @openerpweb.jsonrequest
487 def add_custom(self, request, view_id, arch):
488 CustomView = request.session.model('ir.ui.view.custom')
490 'user_id': request.session._uid,
494 return {'result': True}
496 @openerpweb.jsonrequest
497 def undo_custom(self, request, view_id, reset=False):
498 CustomView = request.session.model('ir.ui.view.custom')
499 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)])
502 CustomView.unlink(vcustom)
504 CustomView.unlink([vcustom[0]])
505 return {'result': True}
506 return {'result': False}
508 def normalize_attrs(self, elem, context):
509 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
510 the client only has to deal with @attrs.
512 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
515 :param elem: the current view node (Python object)
516 :type elem: xml.etree.ElementTree.Element
517 :param dict context: evaluation context
519 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
520 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
521 if 'states' in elem.attrib:
522 attrs.setdefault('invisible', [])\
523 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
525 elem.set('attrs', simplejson.dumps(attrs))
526 for a in ['invisible', 'readonly', 'required']:
528 # In the XML we trust
529 avalue = bool(eval(elem.get(a, 'False'),
530 {'context': context or {}}))
535 if a == 'invisible' and 'attrs' in elem.attrib:
536 del elem.attrib['attrs']
538 def transform_view(self, view_string, session, context=None):
539 # transform nodes on the fly via iterparse, instead of
540 # doing it statically on the parsing result
541 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
543 for event, elem in parser:
547 self.normalize_attrs(elem, context)
548 self.parse_domains_and_contexts(elem, session)
551 def parse_domain(self, elem, attr_name, session):
552 """ Parses an attribute of the provided name as a domain, transforms it
553 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
555 :param elem: the node being parsed
556 :type param: xml.etree.ElementTree.Element
557 :param str attr_name: the name of the attribute which should be parsed
558 :param session: Current OpenERP session
559 :type session: openerpweb.openerpweb.OpenERPSession
561 domain = elem.get(attr_name, '').strip()
566 openerpweb.ast.literal_eval(
571 openerpweb.nonliterals.Domain(session, domain))
573 def parse_domains_and_contexts(self, elem, session):
574 """ Converts domains and contexts from the view into Python objects,
575 either literals if they can be parsed by literal_eval or a special
576 placeholder object if the domain or context refers to free variables.
578 :param elem: the current node being parsed
579 :type param: xml.etree.ElementTree.Element
580 :param session: OpenERP session object, used to store and retrieve
582 :type session: openerpweb.openerpweb.OpenERPSession
584 self.parse_domain(elem, 'domain', session)
585 self.parse_domain(elem, 'filter_domain', session)
586 context_string = elem.get('context', '').strip()
590 openerpweb.ast.literal_eval(context_string))
593 openerpweb.nonliterals.Context(
594 session, context_string))
596 class FormView(View):
597 _cp_path = "/base/formview"
599 @openerpweb.jsonrequest
600 def load(self, req, model, view_id, toolbar=False):
601 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
602 return {'fields_view': fields_view}
604 class ListView(View):
605 _cp_path = "/base/listview"
607 @openerpweb.jsonrequest
608 def load(self, req, model, view_id, toolbar=False):
609 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
610 return {'fields_view': fields_view}
612 def process_colors(self, view, row, context):
613 colors = view['arch']['attrs'].get('colors')
620 for pair in colors.split(';')
621 if eval(pair.split(':')[1], dict(context, **row))
626 elif len(color) == 1:
630 class SearchView(View):
631 _cp_path = "/base/searchview"
633 @openerpweb.jsonrequest
634 def load(self, req, model, view_id):
635 fields_view = self.fields_view_get(req, model, view_id, 'search')
636 return {'fields_view': fields_view}
638 @openerpweb.jsonrequest
639 def fields_get(self, req, model):
640 Model = req.session.model(model)
641 fields = Model.fields_get()
642 return {'fields': fields}
644 class Binary(openerpweb.Controller):
645 _cp_path = "/base/binary"
647 @openerpweb.httprequest
648 def image(self, request, session_id, model, id, field, **kw):
649 cherrypy.response.headers['Content-Type'] = 'image/png'
650 Model = request.session.model(model)
653 res = Model.default_get([field], request.context).get(field, '')
655 res = Model.read([int(id)], [field], request.context)[0].get(field, '')
656 return base64.decodestring(res)
657 except: # TODO: what's the exception here?
658 return self.placeholder()
659 def placeholder(self):
660 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
662 @openerpweb.httprequest
663 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
664 Model = request.session.model(model)
665 res = Model.read([int(id)], [field, fieldname])[0]
666 filecontent = res.get(field, '')
668 raise cherrypy.NotFound
670 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
671 filename = '%s_%s' % (model.replace('.', '_'), id)
673 filename = res.get(fieldname, '') or filename
674 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
675 return base64.decodestring(filecontent)
677 @openerpweb.httprequest
678 def upload(self, request, session_id, callback, ufile=None):
679 cherrypy.response.timeout = 500
681 for key, val in cherrypy.request.headers.iteritems():
682 headers[key.lower()] = val
683 size = int(headers.get('content-length', 0))
684 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
686 out = """<script language="javascript" type="text/javascript">
687 var win = window.top.window,
689 if (typeof(callback) === 'function') {
690 callback.apply(this, %s);
692 win.jQuery('#oe_notification', win.document).notify('create', {
693 title: "Ajax File Upload",
694 text: "Could not find callback"
698 data = ufile.file.read()
699 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
701 args = [False, e.message]
702 return out % (simplejson.dumps(callback), simplejson.dumps(args))
704 @openerpweb.httprequest
705 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
706 cherrypy.response.timeout = 500
707 Model = request.session.model('ir.attachment')
709 out = """<script language="javascript" type="text/javascript">
710 var win = window.top.window,
712 if (typeof(callback) === 'function') {
713 callback.call(this, %s);
716 attachment_id = Model.create({
717 'name': ufile.filename,
718 'datas': base64.encodestring(ufile.file.read()),
723 'filename': ufile.filename,
727 args = { 'error': e.message }
728 return out % (simplejson.dumps(callback), simplejson.dumps(args))
730 class Action(openerpweb.Controller):
731 _cp_path = "/base/action"
733 @openerpweb.jsonrequest
734 def load(self, req, action_id):
735 Actions = req.session.model('ir.actions.actions')
737 action_type = Actions.read([action_id], ['type'], req.session.context)
739 action = req.session.model(action_type[0]['type']).read([action_id], False, req.session.context)
741 value = clean_action(action[0], req.session)
742 return {'result': value}