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 @openerpweb.jsonrequest
454 def name_search(self, req, model, search_str, domain=[], context={}, limit=False):
455 m = req.session.model(model)
456 r = m.name_search(search_str, domain, 'ilike', context, limit)
459 class DataGroup(openerpweb.Controller):
460 _cp_path = "/base/group"
461 @openerpweb.jsonrequest
462 def read(self, request, model, group_by_fields, domain=None):
463 Model = request.session.model(model)
465 return Model.read_group(
466 domain or [], False, group_by_fields, 0, False,
467 dict(request.context, group_by=group_by_fields))
469 class View(openerpweb.Controller):
470 _cp_path = "/base/view"
472 def fields_view_get(self, request, model, view_id, view_type,
473 transform=True, toolbar=False, submenu=False):
474 Model = request.session.model(model)
475 fvg = Model.fields_view_get(view_id, view_type, request.context,
477 self.process_view(request.session, fvg, request.context, transform)
480 def process_view(self, session, fvg, context, transform):
482 evaluation_context = session.evaluation_context(context or {})
483 xml = self.transform_view(fvg['arch'], session, evaluation_context)
485 xml = ElementTree.fromstring(fvg['arch'])
486 fvg['arch'] = Xml2Json.convert_element(xml)
487 for field in fvg['fields'].values():
488 if field.has_key('views') and field['views']:
489 for view in field["views"].values():
490 self.process_view(session, view, None, transform)
492 @openerpweb.jsonrequest
493 def add_custom(self, request, view_id, arch):
494 CustomView = request.session.model('ir.ui.view.custom')
496 'user_id': request.session._uid,
500 return {'result': True}
502 @openerpweb.jsonrequest
503 def undo_custom(self, request, view_id, reset=False):
504 CustomView = request.session.model('ir.ui.view.custom')
505 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)])
508 CustomView.unlink(vcustom)
510 CustomView.unlink([vcustom[0]])
511 return {'result': True}
512 return {'result': False}
514 def normalize_attrs(self, elem, context):
515 """ Normalize @attrs, @invisible, @required, @readonly and @states, so
516 the client only has to deal with @attrs.
518 See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
521 :param elem: the current view node (Python object)
522 :type elem: xml.etree.ElementTree.Element
523 :param dict context: evaluation context
525 # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
526 attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
527 if 'states' in elem.attrib:
528 attrs.setdefault('invisible', [])\
529 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
531 elem.set('attrs', simplejson.dumps(attrs))
532 for a in ['invisible', 'readonly', 'required']:
534 # In the XML we trust
535 avalue = bool(eval(elem.get(a, 'False'),
536 {'context': context or {}}))
541 if a == 'invisible' and 'attrs' in elem.attrib:
542 del elem.attrib['attrs']
544 def transform_view(self, view_string, session, context=None):
545 # transform nodes on the fly via iterparse, instead of
546 # doing it statically on the parsing result
547 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
549 for event, elem in parser:
553 self.normalize_attrs(elem, context)
554 self.parse_domains_and_contexts(elem, session)
557 def parse_domain(self, elem, attr_name, session):
558 """ Parses an attribute of the provided name as a domain, transforms it
559 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
561 :param elem: the node being parsed
562 :type param: xml.etree.ElementTree.Element
563 :param str attr_name: the name of the attribute which should be parsed
564 :param session: Current OpenERP session
565 :type session: openerpweb.openerpweb.OpenERPSession
567 domain = elem.get(attr_name, '').strip()
572 openerpweb.ast.literal_eval(
577 openerpweb.nonliterals.Domain(session, domain))
579 def parse_domains_and_contexts(self, elem, session):
580 """ Converts domains and contexts from the view into Python objects,
581 either literals if they can be parsed by literal_eval or a special
582 placeholder object if the domain or context refers to free variables.
584 :param elem: the current node being parsed
585 :type param: xml.etree.ElementTree.Element
586 :param session: OpenERP session object, used to store and retrieve
588 :type session: openerpweb.openerpweb.OpenERPSession
590 self.parse_domain(elem, 'domain', session)
591 self.parse_domain(elem, 'filter_domain', session)
592 context_string = elem.get('context', '').strip()
596 openerpweb.ast.literal_eval(context_string))
599 openerpweb.nonliterals.Context(
600 session, context_string))
602 class FormView(View):
603 _cp_path = "/base/formview"
605 @openerpweb.jsonrequest
606 def load(self, req, model, view_id, toolbar=False):
607 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
608 return {'fields_view': fields_view}
610 class ListView(View):
611 _cp_path = "/base/listview"
613 @openerpweb.jsonrequest
614 def load(self, req, model, view_id, toolbar=False):
615 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
616 return {'fields_view': fields_view}
618 def process_colors(self, view, row, context):
619 colors = view['arch']['attrs'].get('colors')
626 for pair in colors.split(';')
627 if eval(pair.split(':')[1], dict(context, **row))
632 elif len(color) == 1:
636 class SearchView(View):
637 _cp_path = "/base/searchview"
639 @openerpweb.jsonrequest
640 def load(self, req, model, view_id):
641 fields_view = self.fields_view_get(req, model, view_id, 'search')
642 return {'fields_view': fields_view}
644 @openerpweb.jsonrequest
645 def fields_get(self, req, model):
646 Model = req.session.model(model)
647 fields = Model.fields_get()
648 return {'fields': fields}
650 class Binary(openerpweb.Controller):
651 _cp_path = "/base/binary"
653 @openerpweb.httprequest
654 def image(self, request, session_id, model, id, field, **kw):
655 cherrypy.response.headers['Content-Type'] = 'image/png'
656 Model = request.session.model(model)
659 res = Model.default_get([field], request.context).get(field, '')
661 res = Model.read([int(id)], [field], request.context)[0].get(field, '')
662 return base64.decodestring(res)
663 except: # TODO: what's the exception here?
664 return self.placeholder()
665 def placeholder(self):
666 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
668 @openerpweb.httprequest
669 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
670 Model = request.session.model(model)
671 res = Model.read([int(id)], [field, fieldname])[0]
672 filecontent = res.get(field, '')
674 raise cherrypy.NotFound
676 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
677 filename = '%s_%s' % (model.replace('.', '_'), id)
679 filename = res.get(fieldname, '') or filename
680 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
681 return base64.decodestring(filecontent)
683 @openerpweb.httprequest
684 def upload(self, request, session_id, callback, ufile=None):
685 cherrypy.response.timeout = 500
687 for key, val in cherrypy.request.headers.iteritems():
688 headers[key.lower()] = val
689 size = int(headers.get('content-length', 0))
690 # TODO: might be usefull to have a configuration flag for max-lenght file uploads
692 out = """<script language="javascript" type="text/javascript">
693 var win = window.top.window,
695 if (typeof(callback) === 'function') {
696 callback.apply(this, %s);
698 win.jQuery('#oe_notification', win.document).notify('create', {
699 title: "Ajax File Upload",
700 text: "Could not find callback"
704 data = ufile.file.read()
705 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
707 args = [False, e.message]
708 return out % (simplejson.dumps(callback), simplejson.dumps(args))
710 @openerpweb.httprequest
711 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
712 cherrypy.response.timeout = 500
713 Model = request.session.model('ir.attachment')
715 out = """<script language="javascript" type="text/javascript">
716 var win = window.top.window,
718 if (typeof(callback) === 'function') {
719 callback.call(this, %s);
722 attachment_id = Model.create({
723 'name': ufile.filename,
724 'datas': base64.encodestring(ufile.file.read()),
729 'filename': ufile.filename,
733 args = { 'error': e.message }
734 return out % (simplejson.dumps(callback), simplejson.dumps(args))
736 class Action(openerpweb.Controller):
737 _cp_path = "/base/action"
739 @openerpweb.jsonrequest
740 def load(self, req, action_id):
741 Actions = req.session.model('ir.actions.actions')
743 action_type = Actions.read([action_id], ['type'], req.session.context)
745 action = req.session.model(action_type[0]['type']).read([action_id], False, req.session.context)
747 value = clean_action(action[0], req.session)
748 return {'result': value}