1 # -*- coding: utf-8 -*-
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
19 import web.common.dispatch as openerpweb
21 import web.common.nonliterals
22 openerpweb.ast = web.common.ast
23 openerpweb.nonliterals = web.common.nonliterals
25 from babel.messages.pofile import read_po
27 # Should move to openerpweb.Xml2Json
30 # Simple and straightforward XML-to-JSON converter in Python
33 # URL: http://code.google.com/p/xml2json-direct/
35 def convert_to_json(s):
36 return simplejson.dumps(
37 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
40 def convert_to_structure(s):
41 root = ElementTree.fromstring(s)
42 return Xml2Json.convert_element(root)
45 def convert_element(el, skip_whitespaces=True):
48 ns, name = el.tag.rsplit("}", 1)
50 res["namespace"] = ns[1:]
54 for k, v in el.items():
57 if el.text and (not skip_whitespaces or el.text.strip() != ''):
60 kids.append(Xml2Json.convert_element(kid))
61 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
63 res["children"] = kids
66 #----------------------------------------------------------
67 # OpenERP Web web Controllers
68 #----------------------------------------------------------
70 def manifest_glob(addons_path, addons, key):
73 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
74 for pattern in globlist:
75 for path in glob.glob(os.path.join(addons_path, addon, pattern)):
76 files.append(path[len(addons_path):])
79 def concat_files(addons_path, file_list):
80 """ Concatenate file content
81 return (concat,timestamp)
82 concat: concatenation of file content
83 timestamp: max(os.path.getmtime of file_list)
88 fname = os.path.join(addons_path, i[1:])
89 ftime = os.path.getmtime(fname)
90 if ftime > files_timestamp:
91 files_timestamp = ftime
92 files_content.append(open(fname).read())
93 files_concat = "".join(files_content)
94 return files_concat,files_timestamp
96 home_template = textwrap.dedent("""<!DOCTYPE html>
97 <html style="height: 100%%">
99 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
100 <title>OpenERP</title>
101 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
104 <script type="text/javascript">
106 var c = new openerp.init();
107 var wc = new c.web.WebClient("oe");
112 <body id="oe" class="openerp"></body>
115 class WebClient(openerpweb.Controller):
116 _cp_path = "/web/webclient"
118 @openerpweb.jsonrequest
119 def csslist(self, req, mods='web'):
120 return manifest_glob(req.config.addons_path, mods.split(','), 'css')
122 @openerpweb.jsonrequest
123 def jslist(self, req, mods='web'):
124 return manifest_glob(req.config.addons_path, mods.split(','), 'js')
126 @openerpweb.httprequest
127 def css(self, req, mods='web'):
128 files = manifest_glob(req.config.addons_path, mods.split(','), 'css')
129 content,timestamp = concat_files(req.config.addons_path, files)
130 # TODO request set the Date of last modif and Etag
131 return req.make_response(content, [('Content-Type', 'text/css')])
133 @openerpweb.httprequest
134 def js(self, req, mods='web'):
135 files = manifest_glob(req.config.addons_path, mods.split(','), 'js')
136 content,timestamp = concat_files(req.config.addons_path, files)
137 # TODO request set the Date of last modif and Etag
138 return req.make_response(content, [('Content-Type', 'application/javascript')])
140 @openerpweb.httprequest
141 def home(self, req, s_action=None, **kw):
143 jslist = ['/web/webclient/js']
145 jslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'js')]
146 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
149 csslist = ['/web/webclient/css']
151 csslist = [i + '?debug=' + str(time.time()) for i in manifest_glob(req.config.addons_path, ['web'], 'css')]
152 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
153 r = home_template % {
159 @openerpweb.jsonrequest
160 def translations(self, req, mods, lang):
161 lang_model = req.session.model('res.lang')
162 ids = lang_model.search([("code", "=", lang)])
164 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
165 "grouping", "decimal_point", "thousands_sep"])
169 if lang.count("_") > 0:
173 langs = lang.split(separator)
174 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
177 for addon_name in mods:
178 transl = {"messages":[]}
179 transs[addon_name] = transl
181 f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po")
182 if not os.path.exists(f_name):
185 with open(f_name) as t_file:
190 if x.id and x.string:
191 transl["messages"].append({'id': x.id, 'string': x.string})
192 return {"modules": transs,
193 "lang_parameters": lang_obj}
195 @openerpweb.jsonrequest
196 def version_info(self, req):
198 "version": webrelease.version
201 class Database(openerpweb.Controller):
202 _cp_path = "/web/database"
204 @openerpweb.jsonrequest
205 def get_list(self, req):
206 proxy = req.session.proxy("db")
208 h = req.httprequest.headers['Host'].split(':')[0]
210 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
211 dbs = [i for i in dbs if re.match(r, i)]
212 return {"db_list": dbs}
214 @openerpweb.jsonrequest
215 def progress(self, req, password, id):
216 return req.session.proxy('db').get_progress(password, id)
218 @openerpweb.jsonrequest
219 def create(self, req, fields):
221 params = dict(map(operator.itemgetter('name', 'value'), fields))
223 params['super_admin_pwd'],
225 bool(params.get('demo_data')),
227 params['create_admin_pwd']
231 return req.session.proxy("db").create(*create_attrs)
232 except xmlrpclib.Fault, e:
233 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
234 return {'error': e.faultCode, 'title': 'Create Database'}
235 return {'error': 'Could not create database !', 'title': 'Create Database'}
237 @openerpweb.jsonrequest
238 def drop(self, req, fields):
239 password, db = operator.itemgetter(
240 'drop_pwd', 'drop_db')(
241 dict(map(operator.itemgetter('name', 'value'), fields)))
244 return req.session.proxy("db").drop(password, db)
245 except xmlrpclib.Fault, e:
246 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
247 return {'error': e.faultCode, 'title': 'Drop Database'}
248 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
250 @openerpweb.httprequest
251 def backup(self, req, backup_db, backup_pwd, token):
253 db_dump = base64.b64decode(
254 req.session.proxy("db").dump(backup_pwd, backup_db))
255 return req.make_response(db_dump,
256 [('Content-Type', 'application/octet-stream; charset=binary'),
257 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
258 {'fileToken': int(token)}
260 except xmlrpclib.Fault, e:
261 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
262 return 'Backup Database|' + e.faultCode
263 return 'Backup Database|Could not generate database backup'
265 @openerpweb.httprequest
266 def restore(self, req, db_file, restore_pwd, new_db):
268 data = base64.b64encode(db_file.file.read())
269 req.session.proxy("db").restore(restore_pwd, new_db, data)
271 except xmlrpclib.Fault, e:
272 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
273 raise Exception("AccessDenied")
275 @openerpweb.jsonrequest
276 def change_password(self, req, fields):
277 old_password, new_password = operator.itemgetter(
278 'old_pwd', 'new_pwd')(
279 dict(map(operator.itemgetter('name', 'value'), fields)))
281 return req.session.proxy("db").change_admin_password(old_password, new_password)
282 except xmlrpclib.Fault, e:
283 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
284 return {'error': e.faultCode, 'title': 'Change Password'}
285 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
287 class Session(openerpweb.Controller):
288 _cp_path = "/web/session"
290 @openerpweb.jsonrequest
291 def login(self, req, db, login, password):
292 req.session.login(db, login, password)
293 ctx = req.session.get_context()
296 "session_id": req.session_id,
297 "uid": req.session._uid,
300 @openerpweb.jsonrequest
301 def change_password (self,req,fields):
302 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
303 dict(map(operator.itemgetter('name', 'value'), fields)))
304 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
305 return {'error':'All passwords have to be filled.','title': 'Change Password'}
306 if new_password != confirm_password:
307 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
309 if req.session.model('res.users').change_password(
310 old_password, new_password):
311 return {'new_password':new_password}
313 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
314 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
315 @openerpweb.jsonrequest
316 def sc_list(self, req):
317 return req.session.model('ir.ui.view_sc').get_sc(
318 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
320 @openerpweb.jsonrequest
321 def get_lang_list(self, req):
324 'lang_list': (req.session.proxy("db").list_lang() or []),
328 return {"error": e, "title": "Languages"}
330 @openerpweb.jsonrequest
331 def modules(self, req):
332 # TODO query server for installed web modules
334 for name, manifest in openerpweb.addons_manifest.items():
335 if name != 'web' and manifest.get('active', True):
339 @openerpweb.jsonrequest
340 def eval_domain_and_context(self, req, contexts, domains,
342 """ Evaluates sequences of domains and contexts, composing them into
343 a single context, domain or group_by sequence.
345 :param list contexts: list of contexts to merge together. Contexts are
346 evaluated in sequence, all previous contexts
347 are part of their own evaluation context
348 (starting at the session context).
349 :param list domains: list of domains to merge together. Domains are
350 evaluated in sequence and appended to one another
351 (implicit AND), their evaluation domain is the
352 result of merging all contexts.
353 :param list group_by_seq: list of domains (which may be in a different
354 order than the ``contexts`` parameter),
355 evaluated in sequence, their ``'group_by'``
356 key is extracted if they have one.
361 the global context created by merging all of
365 the concatenation of all domains
368 a list of fields to group by, potentially empty (in which case
369 no group by should be performed)
371 context, domain = eval_context_and_domain(req.session,
372 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
373 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
375 group_by_sequence = []
376 for candidate in (group_by_seq or []):
377 ctx = req.session.eval_context(candidate, context)
378 group_by = ctx.get('group_by')
381 elif isinstance(group_by, basestring):
382 group_by_sequence.append(group_by)
384 group_by_sequence.extend(group_by)
389 'group_by': group_by_sequence
392 @openerpweb.jsonrequest
393 def save_session_action(self, req, the_action):
395 This method store an action object in the session object and returns an integer
396 identifying that action. The method get_session_action() can be used to get
399 :param the_action: The action to save in the session.
400 :type the_action: anything
401 :return: A key identifying the saved action.
404 saved_actions = req.httpsession.get('saved_actions')
405 if not saved_actions:
406 saved_actions = {"next":0, "actions":{}}
407 req.httpsession['saved_actions'] = saved_actions
408 # we don't allow more than 10 stored actions
409 if len(saved_actions["actions"]) >= 10:
410 del saved_actions["actions"][min(saved_actions["actions"].keys())]
411 key = saved_actions["next"]
412 saved_actions["actions"][key] = the_action
413 saved_actions["next"] = key + 1
416 @openerpweb.jsonrequest
417 def get_session_action(self, req, key):
419 Gets back a previously saved action. This method can return None if the action
420 was saved since too much time (this case should be handled in a smart way).
422 :param key: The key given by save_session_action()
424 :return: The saved action or None.
427 saved_actions = req.httpsession.get('saved_actions')
428 if not saved_actions:
430 return saved_actions["actions"].get(key)
432 @openerpweb.jsonrequest
433 def check(self, req):
434 req.session.assert_valid()
437 def eval_context_and_domain(session, context, domain=None):
438 e_context = session.eval_context(context)
439 # should we give the evaluated context as an evaluation context to the domain?
440 e_domain = session.eval_domain(domain or [])
442 return e_context, e_domain
444 def load_actions_from_ir_values(req, key, key2, models, meta):
445 context = req.session.eval_context(req.context)
446 Values = req.session.model('ir.values')
447 actions = Values.get(key, key2, models, meta, context)
449 return [(id, name, clean_action(req, action))
450 for id, name, action in actions]
452 def clean_action(req, action):
453 action.setdefault('flags', {})
455 context = req.session.eval_context(req.context)
456 eval_ctx = req.session.evaluation_context(context)
458 # values come from the server, we can just eval them
459 if isinstance(action.get('context'), basestring):
460 action['context'] = eval( action['context'], eval_ctx ) or {}
462 if isinstance(action.get('domain'), basestring):
463 action['domain'] = eval( action['domain'], eval_ctx ) or []
465 if action['type'] == 'ir.actions.act_window':
466 return fix_view_modes(action)
469 # I think generate_views,fix_view_modes should go into js ActionManager
470 def generate_views(action):
472 While the server generates a sequence called "views" computing dependencies
473 between a bunch of stuff for views coming directly from the database
474 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
475 to return custom view dictionaries generated on the fly.
477 In that case, there is no ``views`` key available on the action.
479 Since the web client relies on ``action['views']``, generate it here from
480 ``view_mode`` and ``view_id``.
482 Currently handles two different cases:
484 * no view_id, multiple view_mode
485 * single view_id, single view_mode
487 :param dict action: action descriptor dictionary to generate a views key for
489 view_id = action.get('view_id', False)
490 if isinstance(view_id, (list, tuple)):
493 # providing at least one view mode is a requirement, not an option
494 view_modes = action['view_mode'].split(',')
496 if len(view_modes) > 1:
498 raise ValueError('Non-db action dictionaries should provide '
499 'either multiple view modes or a single view '
500 'mode and an optional view id.\n\n Got view '
501 'modes %r and view id %r for action %r' % (
502 view_modes, view_id, action))
503 action['views'] = [(False, mode) for mode in view_modes]
505 action['views'] = [(view_id, view_modes[0])]
507 def fix_view_modes(action):
508 """ For historical reasons, OpenERP has weird dealings in relation to
509 view_mode and the view_type attribute (on window actions):
511 * one of the view modes is ``tree``, which stands for both list views
513 * the choice is made by checking ``view_type``, which is either
514 ``form`` for a list view or ``tree`` for an actual tree view
516 This methods simply folds the view_type into view_mode by adding a
517 new view mode ``list`` which is the result of the ``tree`` view_mode
518 in conjunction with the ``form`` view_type.
520 TODO: this should go into the doc, some kind of "peculiarities" section
522 :param dict action: an action descriptor
523 :returns: nothing, the action is modified in place
525 if 'views' not in action:
526 generate_views(action)
528 if action.pop('view_type') != 'form':
532 [id, mode if mode != 'tree' else 'list']
533 for id, mode in action['views']
538 class Menu(openerpweb.Controller):
539 _cp_path = "/web/menu"
541 @openerpweb.jsonrequest
543 return {'data': self.do_load(req)}
545 def do_load(self, req):
546 """ Loads all menu items (all applications and their sub-menus).
548 :param req: A request object, with an OpenERP session attribute
549 :type req: < session -> OpenERPSession >
550 :return: the menu root
551 :rtype: dict('children': menu_nodes)
553 Menus = req.session.model('ir.ui.menu')
554 # menus are loaded fully unlike a regular tree view, cause there are
555 # less than 512 items
556 context = req.session.eval_context(req.context)
557 menu_ids = Menus.search([], 0, False, False, context)
558 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
559 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
560 menu_items.append(menu_root)
562 # make a tree using parent_id
563 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
564 for menu_item in menu_items:
565 if menu_item['parent_id']:
566 parent = menu_item['parent_id'][0]
569 if parent in menu_items_map:
570 menu_items_map[parent].setdefault(
571 'children', []).append(menu_item)
573 # sort by sequence a tree using parent_id
574 for menu_item in menu_items:
575 menu_item.setdefault('children', []).sort(
576 key=lambda x:x["sequence"])
580 @openerpweb.jsonrequest
581 def action(self, req, menu_id):
582 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
583 [('ir.ui.menu', menu_id)], False)
584 return {"action": actions}
586 class DataSet(openerpweb.Controller):
587 _cp_path = "/web/dataset"
589 @openerpweb.jsonrequest
590 def fields(self, req, model):
591 return {'fields': req.session.model(model).fields_get(False,
592 req.session.eval_context(req.context))}
594 @openerpweb.jsonrequest
595 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
596 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
597 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
599 """ Performs a search() followed by a read() (if needed) using the
600 provided search criteria
602 :param req: a JSON-RPC request object
603 :type req: openerpweb.JsonRequest
604 :param str model: the name of the model to search on
605 :param fields: a list of the fields to return in the result records
607 :param int offset: from which index should the results start being returned
608 :param int limit: the maximum number of records to return
609 :param list domain: the search domain for the query
610 :param list sort: sorting directives
611 :returns: A structure (dict) with two keys: ids (all the ids matching
612 the (domain, context) pair) and records (paginated records
613 matching fields selection set)
616 Model = req.session.model(model)
618 context, domain = eval_context_and_domain(
619 req.session, req.context, domain)
621 ids = Model.search(domain, 0, False, sort or False, context)
622 # need to fill the dataset with all ids for the (domain, context) pair,
623 # so search un-paginated and paginate manually before reading
624 paginated_ids = ids[offset:(offset + limit if limit else None)]
625 if fields and fields == ['id']:
626 # shortcut read if we only want the ids
629 'records': map(lambda id: {'id': id}, paginated_ids)
632 records = Model.read(paginated_ids, fields or False, context)
633 records.sort(key=lambda obj: ids.index(obj['id']))
640 @openerpweb.jsonrequest
641 def read(self, req, model, ids, fields=False):
642 return self.do_search_read(req, model, ids, fields)
644 @openerpweb.jsonrequest
645 def get(self, req, model, ids, fields=False):
646 return self.do_get(req, model, ids, fields)
648 def do_get(self, req, model, ids, fields=False):
649 """ Fetches and returns the records of the model ``model`` whose ids
652 The results are in the same order as the inputs, but elements may be
653 missing (if there is no record left for the id)
655 :param req: the JSON-RPC2 request object
656 :type req: openerpweb.JsonRequest
657 :param model: the model to read from
659 :param ids: a list of identifiers
661 :param fields: a list of fields to fetch, ``False`` or empty to fetch
662 all fields in the model
663 :type fields: list | False
664 :returns: a list of records, in the same order as the list of ids
667 Model = req.session.model(model)
668 records = Model.read(ids, fields, req.session.eval_context(req.context))
670 record_map = dict((record['id'], record) for record in records)
672 return [record_map[id] for id in ids if record_map.get(id)]
674 @openerpweb.jsonrequest
675 def load(self, req, model, id, fields):
676 m = req.session.model(model)
678 r = m.read([id], False, req.session.eval_context(req.context))
681 return {'value': value}
683 @openerpweb.jsonrequest
684 def create(self, req, model, data):
685 m = req.session.model(model)
686 r = m.create(data, req.session.eval_context(req.context))
689 @openerpweb.jsonrequest
690 def save(self, req, model, id, data):
691 m = req.session.model(model)
692 r = m.write([id], data, req.session.eval_context(req.context))
695 @openerpweb.jsonrequest
696 def unlink(self, req, model, ids=()):
697 Model = req.session.model(model)
698 return Model.unlink(ids, req.session.eval_context(req.context))
700 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
701 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
702 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
703 c, d = eval_context_and_domain(req.session, context, domain)
704 if domain_id and len(args) - 1 >= domain_id:
706 if context_id and len(args) - 1 >= context_id:
709 for i in xrange(len(args)):
710 if isinstance(args[i], web.common.nonliterals.BaseContext):
711 args[i] = req.session.eval_context(args[i])
712 if isinstance(args[i], web.common.nonliterals.BaseDomain):
713 args[i] = req.session.eval_domain(args[i])
715 return getattr(req.session.model(model), method)(*args)
717 @openerpweb.jsonrequest
718 def call(self, req, model, method, args, domain_id=None, context_id=None):
719 return self.call_common(req, model, method, args, domain_id, context_id)
721 @openerpweb.jsonrequest
722 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
723 action = self.call_common(req, model, method, args, domain_id, context_id)
724 if isinstance(action, dict) and action.get('type') != '':
725 return {'result': clean_action(req, action)}
726 return {'result': False}
728 @openerpweb.jsonrequest
729 def exec_workflow(self, req, model, id, signal):
730 r = req.session.exec_workflow(model, id, signal)
733 @openerpweb.jsonrequest
734 def default_get(self, req, model, fields):
735 Model = req.session.model(model)
736 return Model.default_get(fields, req.session.eval_context(req.context))
738 @openerpweb.jsonrequest
739 def name_search(self, req, model, search_str, domain=[], context={}):
740 m = req.session.model(model)
741 r = m.name_search(search_str+'%', domain, '=ilike', context)
744 class DataGroup(openerpweb.Controller):
745 _cp_path = "/web/group"
746 @openerpweb.jsonrequest
747 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
748 Model = req.session.model(model)
749 context, domain = eval_context_and_domain(req.session, req.context, domain)
751 return Model.read_group(
752 domain or [], fields, group_by_fields, 0, False,
753 dict(context, group_by=group_by_fields), sort or False)
755 class View(openerpweb.Controller):
756 _cp_path = "/web/view"
758 def fields_view_get(self, req, model, view_id, view_type,
759 transform=True, toolbar=False, submenu=False):
760 Model = req.session.model(model)
761 context = req.session.eval_context(req.context)
762 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
763 # todo fme?: check that we should pass the evaluated context here
764 self.process_view(req.session, fvg, context, transform)
765 if toolbar and transform:
766 self.process_toolbar(req, fvg['toolbar'])
769 def process_view(self, session, fvg, context, transform):
770 # depending on how it feels, xmlrpclib.ServerProxy can translate
771 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
772 # enjoy unicode strings which can not be trivially converted to
773 # strings, and it blows up during parsing.
775 # So ensure we fix this retardation by converting view xml back to
777 if isinstance(fvg['arch'], unicode):
778 arch = fvg['arch'].encode('utf-8')
783 evaluation_context = session.evaluation_context(context or {})
784 xml = self.transform_view(arch, session, evaluation_context)
786 xml = ElementTree.fromstring(arch)
787 fvg['arch'] = Xml2Json.convert_element(xml)
789 for field in fvg['fields'].itervalues():
790 if field.get('views'):
791 for view in field["views"].itervalues():
792 self.process_view(session, view, None, transform)
793 if field.get('domain'):
794 field["domain"] = self.parse_domain(field["domain"], session)
795 if field.get('context'):
796 field["context"] = self.parse_context(field["context"], session)
798 def process_toolbar(self, req, toolbar):
800 The toolbar is a mapping of section_key: [action_descriptor]
802 We need to clean all those actions in order to ensure correct
805 for actions in toolbar.itervalues():
806 for action in actions:
807 if 'context' in action:
808 action['context'] = self.parse_context(
809 action['context'], req.session)
810 if 'domain' in action:
811 action['domain'] = self.parse_domain(
812 action['domain'], req.session)
814 @openerpweb.jsonrequest
815 def add_custom(self, req, view_id, arch):
816 CustomView = req.session.model('ir.ui.view.custom')
818 'user_id': req.session._uid,
821 }, req.session.eval_context(req.context))
822 return {'result': True}
824 @openerpweb.jsonrequest
825 def undo_custom(self, req, view_id, reset=False):
826 CustomView = req.session.model('ir.ui.view.custom')
827 context = req.session.eval_context(req.context)
828 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
829 0, False, False, context)
832 CustomView.unlink(vcustom, context)
834 CustomView.unlink([vcustom[0]], context)
835 return {'result': True}
836 return {'result': False}
838 def transform_view(self, view_string, session, context=None):
839 # transform nodes on the fly via iterparse, instead of
840 # doing it statically on the parsing result
841 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
843 for event, elem in parser:
847 self.parse_domains_and_contexts(elem, session)
850 def parse_domain(self, domain, session):
851 """ Parses an arbitrary string containing a domain, transforms it
852 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
854 :param domain: the domain to parse, if the domain is not a string it
855 is assumed to be a literal domain and is returned as-is
856 :param session: Current OpenERP session
857 :type session: openerpweb.openerpweb.OpenERPSession
859 if not isinstance(domain, (str, unicode)):
862 return openerpweb.ast.literal_eval(domain)
865 return openerpweb.nonliterals.Domain(session, domain)
867 def parse_context(self, context, session):
868 """ Parses an arbitrary string containing a context, transforms it
869 to either a literal context or a :class:`openerpweb.nonliterals.Context`
871 :param context: the context to parse, if the context is not a string it
872 is assumed to be a literal domain and is returned as-is
873 :param session: Current OpenERP session
874 :type session: openerpweb.openerpweb.OpenERPSession
876 if not isinstance(context, (str, unicode)):
879 return openerpweb.ast.literal_eval(context)
881 return openerpweb.nonliterals.Context(session, context)
883 def parse_domains_and_contexts(self, elem, session):
884 """ Converts domains and contexts from the view into Python objects,
885 either literals if they can be parsed by literal_eval or a special
886 placeholder object if the domain or context refers to free variables.
888 :param elem: the current node being parsed
889 :type param: xml.etree.ElementTree.Element
890 :param session: OpenERP session object, used to store and retrieve
892 :type session: openerpweb.openerpweb.OpenERPSession
894 for el in ['domain', 'filter_domain']:
895 domain = elem.get(el, '').strip()
897 elem.set(el, self.parse_domain(domain, session))
898 for el in ['context', 'default_get']:
899 context_string = elem.get(el, '').strip()
901 elem.set(el, self.parse_context(context_string, session))
903 @openerpweb.jsonrequest
904 def load(self, req, model, view_id, view_type, toolbar=False):
905 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
907 class ListView(View):
908 _cp_path = "/web/listview"
910 def process_colors(self, view, row, context):
911 colors = view['arch']['attrs'].get('colors')
918 for pair in colors.split(';')
919 if eval(pair.split(':')[1], dict(context, **row))
924 elif len(color) == 1:
928 class TreeView(View):
929 _cp_path = "/web/treeview"
931 @openerpweb.jsonrequest
932 def action(self, req, model, id):
933 return load_actions_from_ir_values(
934 req,'action', 'tree_but_open',[(model, id)],
937 class SearchView(View):
938 _cp_path = "/web/searchview"
940 @openerpweb.jsonrequest
941 def load(self, req, model, view_id):
942 fields_view = self.fields_view_get(req, model, view_id, 'search')
943 return {'fields_view': fields_view}
945 @openerpweb.jsonrequest
946 def fields_get(self, req, model):
947 Model = req.session.model(model)
948 fields = Model.fields_get(False, req.session.eval_context(req.context))
949 for field in fields.values():
950 # shouldn't convert the views too?
951 if field.get('domain'):
952 field["domain"] = self.parse_domain(field["domain"], req.session)
953 if field.get('context'):
954 field["context"] = self.parse_domain(field["context"], req.session)
955 return {'fields': fields}
957 @openerpweb.jsonrequest
958 def get_filters(self, req, model):
959 Model = req.session.model("ir.filters")
960 filters = Model.get_filters(model)
961 for filter in filters:
962 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
963 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
966 @openerpweb.jsonrequest
967 def save_filter(self, req, model, name, context_to_save, domain):
968 Model = req.session.model("ir.filters")
969 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
970 ctx.session = req.session
972 domain = openerpweb.nonliterals.CompoundDomain(domain)
973 domain.session = req.session
974 domain = domain.evaluate()
975 uid = req.session._uid
976 context = req.session.eval_context(req.context)
977 to_return = Model.create_or_replace({"context": ctx,
985 class Binary(openerpweb.Controller):
986 _cp_path = "/web/binary"
988 @openerpweb.httprequest
989 def image(self, req, model, id, field, **kw):
990 Model = req.session.model(model)
991 context = req.session.eval_context(req.context)
995 res = Model.default_get([field], context).get(field, '')
997 res = Model.read([int(id)], [field], context)[0].get(field, '')
998 image_data = base64.b64decode(res)
999 except (TypeError, xmlrpclib.Fault):
1000 image_data = self.placeholder(req)
1001 return req.make_response(image_data, [
1002 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1003 def placeholder(self, req):
1004 return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1006 @openerpweb.httprequest
1007 def saveas(self, req, model, id, field, fieldname, **kw):
1008 Model = req.session.model(model)
1009 context = req.session.eval_context(req.context)
1010 res = Model.read([int(id)], [field, fieldname], context)[0]
1011 filecontent = res.get(field, '')
1013 return req.not_found()
1015 filename = '%s_%s' % (model.replace('.', '_'), id)
1017 filename = res.get(fieldname, '') or filename
1018 return req.make_response(filecontent,
1019 [('Content-Type', 'application/octet-stream'),
1020 ('Content-Disposition', 'attachment; filename=' + filename)])
1022 @openerpweb.httprequest
1023 def upload(self, req, callback, ufile):
1024 # TODO: might be useful to have a configuration flag for max-length file uploads
1026 out = """<script language="javascript" type="text/javascript">
1027 var win = window.top.window,
1029 if (typeof(callback) === 'function') {
1030 callback.apply(this, %s);
1032 win.jQuery('#oe_notification', win.document).notify('create', {
1033 title: "Ajax File Upload",
1034 text: "Could not find callback"
1039 args = [ufile.content_length, ufile.filename,
1040 ufile.content_type, base64.b64encode(data)]
1041 except Exception, e:
1042 args = [False, e.message]
1043 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1045 @openerpweb.httprequest
1046 def upload_attachment(self, req, callback, model, id, ufile):
1047 context = req.session.eval_context(req.context)
1048 Model = req.session.model('ir.attachment')
1050 out = """<script language="javascript" type="text/javascript">
1051 var win = window.top.window,
1053 if (typeof(callback) === 'function') {
1054 callback.call(this, %s);
1057 attachment_id = Model.create({
1058 'name': ufile.filename,
1059 'datas': base64.encodestring(ufile.read()),
1064 'filename': ufile.filename,
1067 except Exception, e:
1068 args = { 'error': e.message }
1069 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1071 class Action(openerpweb.Controller):
1072 _cp_path = "/web/action"
1074 @openerpweb.jsonrequest
1075 def load(self, req, action_id):
1076 Actions = req.session.model('ir.actions.actions')
1078 context = req.session.eval_context(req.context)
1079 action_type = Actions.read([action_id], ['type'], context)
1082 if action_type[0]['type'] == 'ir.actions.report.xml':
1083 ctx.update({'bin_size': True})
1085 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1087 value = clean_action(req, action[0])
1088 return {'result': value}
1090 @openerpweb.jsonrequest
1091 def run(self, req, action_id):
1092 return clean_action(req, req.session.model('ir.actions.server').run(
1093 [action_id], req.session.eval_context(req.context)))
1096 _cp_path = "/web/export"
1098 @openerpweb.jsonrequest
1099 def formats(self, req):
1100 """ Returns all valid export formats
1102 :returns: for each export format, a pair of identifier and printable name
1103 :rtype: [(str, str)]
1107 for path, controller in openerpweb.controllers_path.iteritems()
1108 if path.startswith(self._cp_path)
1109 if hasattr(controller, 'fmt')
1110 ], key=operator.itemgetter(1))
1112 def fields_get(self, req, model):
1113 Model = req.session.model(model)
1114 fields = Model.fields_get(False, req.session.eval_context(req.context))
1117 @openerpweb.jsonrequest
1118 def get_fields(self, req, model, prefix='', parent_name= '',
1119 import_compat=True, parent_field_type=None):
1121 if import_compat and parent_field_type == "many2one":
1124 fields = self.fields_get(req, model)
1125 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1127 fields_sequence = sorted(fields.iteritems(),
1128 key=lambda field: field[1].get('string', ''))
1131 for field_name, field in fields_sequence:
1132 if import_compat and field.get('readonly'):
1133 # If none of the field's states unsets readonly, skip the field
1134 if all(dict(attrs).get('readonly', True)
1135 for attrs in field.get('states', {}).values()):
1138 id = prefix + (prefix and '/'or '') + field_name
1139 name = parent_name + (parent_name and '/' or '') + field['string']
1140 record = {'id': id, 'string': name,
1141 'value': id, 'children': False,
1142 'field_type': field.get('type'),
1143 'required': field.get('required')}
1144 records.append(record)
1146 if len(name.split('/')) < 3 and 'relation' in field:
1147 ref = field.pop('relation')
1148 record['value'] += '/id'
1149 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1151 if not import_compat or field['type'] == 'one2many':
1152 # m2m field in import_compat is childless
1153 record['children'] = True
1157 @openerpweb.jsonrequest
1158 def namelist(self,req, model, export_id):
1159 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1160 export = req.session.model("ir.exports").read([export_id])[0]
1161 export_fields_list = req.session.model("ir.exports.line").read(
1162 export['export_fields'])
1164 fields_data = self.fields_info(
1165 req, model, map(operator.itemgetter('name'), export_fields_list))
1168 {'name': field['name'], 'label': fields_data[field['name']]}
1169 for field in export_fields_list
1172 def fields_info(self, req, model, export_fields):
1174 fields = self.fields_get(req, model)
1175 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1177 # To make fields retrieval more efficient, fetch all sub-fields of a
1178 # given field at the same time. Because the order in the export list is
1179 # arbitrary, this requires ordering all sub-fields of a given field
1180 # together so they can be fetched at the same time
1182 # Works the following way:
1183 # * sort the list of fields to export, the default sorting order will
1184 # put the field itself (if present, for xmlid) and all of its
1185 # sub-fields right after it
1186 # * then, group on: the first field of the path (which is the same for
1187 # a field and for its subfields and the length of splitting on the
1188 # first '/', which basically means grouping the field on one side and
1189 # all of the subfields on the other. This way, we have the field (for
1190 # the xmlid) with length 1, and all of the subfields with the same
1191 # base but a length "flag" of 2
1192 # * if we have a normal field (length 1), just add it to the info
1193 # mapping (with its string) as-is
1194 # * otherwise, recursively call fields_info via graft_subfields.
1195 # all graft_subfields does is take the result of fields_info (on the
1196 # field's model) and prepend the current base (current field), which
1197 # rebuilds the whole sub-tree for the field
1199 # result: because we're not fetching the fields_get for half the
1200 # database models, fetching a namelist with a dozen fields (including
1201 # relational data) falls from ~6s to ~300ms (on the leads model).
1202 # export lists with no sub-fields (e.g. import_compatible lists with
1203 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1204 # there's a single fields_get to execute)
1205 for (base, length), subfields in itertools.groupby(
1206 sorted(export_fields),
1207 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1208 subfields = list(subfields)
1210 # subfields is a seq of $base/*rest, and not loaded yet
1211 info.update(self.graft_subfields(
1212 req, fields[base]['relation'], base, fields[base]['string'],
1216 info[base] = fields[base]['string']
1220 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1221 export_fields = [field.split('/', 1)[1] for field in fields]
1223 (prefix + '/' + k, prefix_string + '/' + v)
1224 for k, v in self.fields_info(req, model, export_fields).iteritems())
1226 #noinspection PyPropertyDefinition
1228 def content_type(self):
1229 """ Provides the format's content type """
1230 raise NotImplementedError()
1232 def filename(self, base):
1233 """ Creates a valid filename for the format (with extension) from the
1234 provided base name (exension-less)
1236 raise NotImplementedError()
1238 def from_data(self, fields, rows):
1239 """ Conversion method from OpenERP's export data to whatever the
1240 current export class outputs
1242 :params list fields: a list of fields to export
1243 :params list rows: a list of records to export
1247 raise NotImplementedError()
1249 @openerpweb.httprequest
1250 def index(self, req, data, token):
1251 model, fields, ids, domain, import_compat = \
1252 operator.itemgetter('model', 'fields', 'ids', 'domain',
1254 simplejson.loads(data))
1256 context = req.session.eval_context(req.context)
1257 Model = req.session.model(model)
1258 ids = ids or Model.search(domain, context=context)
1260 field_names = map(operator.itemgetter('name'), fields)
1261 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1264 columns_headers = field_names
1266 columns_headers = [val['label'].strip() for val in fields]
1269 return req.make_response(self.from_data(columns_headers, import_data),
1270 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1271 ('Content-Type', self.content_type)],
1272 cookies={'fileToken': int(token)})
1274 class CSVExport(Export):
1275 _cp_path = '/web/export/csv'
1276 fmt = ('csv', 'CSV')
1279 def content_type(self):
1280 return 'text/csv;charset=utf8'
1282 def filename(self, base):
1283 return base + '.csv'
1285 def from_data(self, fields, rows):
1287 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1289 writer.writerow(fields)
1294 if isinstance(d, basestring):
1295 d = d.replace('\n',' ').replace('\t',' ')
1297 d = d.encode('utf-8')
1300 if d is False: d = None
1302 writer.writerow(row)
1309 class ExcelExport(Export):
1310 _cp_path = '/web/export/xls'
1311 fmt = ('xls', 'Excel')
1314 def content_type(self):
1315 return 'application/vnd.ms-excel'
1317 def filename(self, base):
1318 return base + '.xls'
1320 def from_data(self, fields, rows):
1323 workbook = xlwt.Workbook()
1324 worksheet = workbook.add_sheet('Sheet 1')
1326 for i, fieldname in enumerate(fields):
1327 worksheet.write(0, i, str(fieldname))
1328 worksheet.col(i).width = 8000 # around 220 pixels
1330 style = xlwt.easyxf('align: wrap yes')
1332 for row_index, row in enumerate(rows):
1333 for cell_index, cell_value in enumerate(row):
1334 if isinstance(cell_value, basestring):
1335 cell_value = re.sub("\r", " ", cell_value)
1336 worksheet.write(row_index + 1, cell_index, cell_value, style)
1345 class Reports(View):
1346 _cp_path = "/web/report"
1347 POLLING_DELAY = 0.25
1349 'doc': 'application/vnd.ms-word',
1350 'html': 'text/html',
1351 'odt': 'application/vnd.oasis.opendocument.text',
1352 'pdf': 'application/pdf',
1353 'sxw': 'application/vnd.sun.xml.writer',
1354 'xls': 'application/vnd.ms-excel',
1357 @openerpweb.httprequest
1358 def index(self, req, action, token):
1359 action = simplejson.loads(action)
1361 report_srv = req.session.proxy("report")
1362 context = req.session.eval_context(
1363 openerpweb.nonliterals.CompoundContext(
1364 req.context or {}, action[ "context"]))
1367 report_ids = context["active_ids"]
1368 if 'report_type' in action:
1369 report_data['report_type'] = action['report_type']
1370 if 'datas' in action:
1371 if 'form' in action['datas']:
1372 report_data['form'] = action['datas']['form']
1373 if 'ids' in action['datas']:
1374 report_ids = action['datas']['ids']
1375 report_id = report_srv.report(
1376 req.session._db, req.session._uid, req.session._password,
1377 action["report_name"], report_ids,
1378 report_data, context)
1380 report_struct = None
1382 report_struct = report_srv.report_get(
1383 req.session._db, req.session._uid, req.session._password, report_id)
1384 if report_struct["state"]:
1386 time.sleep(self.POLLING_DELAY)
1388 report = base64.b64decode(report_struct['result'])
1389 if report_struct.get('code') == 'zlib':
1390 report = zlib.decompress(report)
1391 report_mimetype = self.TYPES_MAPPING.get(
1392 report_struct['format'], 'octet-stream')
1393 return req.make_response(report,
1395 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1396 ('Content-Type', report_mimetype),
1397 ('Content-Length', len(report))],
1398 cookies={'fileToken': int(token)})