1 # -*- coding: utf-8 -*-
15 from xml.etree import ElementTree
16 from cStringIO import StringIO
18 import babel.messages.pofile
21 openerpweb = web.common.http
23 #----------------------------------------------------------
24 # OpenERP Web web Controllers
25 #----------------------------------------------------------
27 def concat_files(file_list, reader=None):
28 """ Concatenate file content
29 return (concat,timestamp)
30 concat: concatenation of file content, read by `reader`
31 timestamp: max(os.path.getmtime of file_list)
40 for fname in file_list:
41 ftime = os.path.getmtime(fname)
42 if ftime > files_timestamp:
43 files_timestamp = ftime
45 files_content.append(reader(fname))
46 files_concat = "".join(files_content)
47 return files_concat,files_timestamp
49 html_template = """<!DOCTYPE html>
50 <html style="height: 100%%">
52 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
53 <title>OpenERP</title>
54 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
57 <script type="text/javascript">
59 var s = new openerp.init(%(modules)s);
64 <body id="oe" class="openerp"></body>
68 class WebClient(openerpweb.Controller):
69 _cp_path = "/web/webclient"
71 def server_wide_modules(self, req):
72 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
75 def manifest_glob(self, req, addons, key):
77 addons = self.server_wide_modules(req)
79 addons = addons.split(',')
81 manifest = openerpweb.addons_manifest.get(addon, None)
84 # ensure does not ends with /
85 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
86 globlist = manifest.get(key, [])
87 for pattern in globlist:
88 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
89 yield path, path[len(addons_path):]
91 def manifest_list(self, req, mods, extension):
93 path = '/web/webclient/' + extension
95 path += '?mods=' + mods
97 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
99 @openerpweb.jsonrequest
100 def csslist(self, req, mods=None):
101 return self.manifest_list(req, mods, 'css')
103 @openerpweb.jsonrequest
104 def jslist(self, req, mods=None):
105 return self.manifest_list(req, mods, 'js')
107 @openerpweb.httprequest
108 def css(self, req, mods=None):
110 files = list(self.manifest_glob(req, mods, 'css'))
111 file_map = dict(files)
113 rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
114 rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
118 """read the a css file and absolutify all relative uris"""
122 web_path = file_map[f]
123 web_dir = os.path.dirname(web_path)
127 r"""@import \1%s/""" % (web_dir,),
133 r"""url(\1%s/""" % (web_dir,),
138 content,timestamp = concat_files((f[0] for f in files), reader)
139 # TODO use timestamp to set Last mofified date and E-tag
140 return req.make_response(content, [('Content-Type', 'text/css')])
142 @openerpweb.httprequest
143 def js(self, req, mods=None):
144 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
145 content,timestamp = concat_files(files)
146 # TODO use timestamp to set Last mofified date and E-tag
147 return req.make_response(content, [('Content-Type', 'application/javascript')])
149 @openerpweb.httprequest
150 def home(self, req, s_action=None, **kw):
151 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
152 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
154 r = html_template % {
157 'modules': simplejson.dumps(self.server_wide_modules(req)),
158 'init': 'new s.web.WebClient("oe").start();',
162 @openerpweb.jsonrequest
163 def translations(self, req, mods, lang):
164 lang_model = req.session.model('res.lang')
165 ids = lang_model.search([("code", "=", lang)])
167 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
168 "grouping", "decimal_point", "thousands_sep"])
172 if lang.count("_") > 0:
176 langs = lang.split(separator)
177 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
180 for addon_name in mods:
181 transl = {"messages":[]}
182 transs[addon_name] = transl
184 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
185 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
186 if not os.path.exists(f_name):
189 with open(f_name) as t_file:
190 po = babel.messages.pofile.read_po(t_file)
194 if x.id and x.string:
195 transl["messages"].append({'id': x.id, 'string': x.string})
196 return {"modules": transs,
197 "lang_parameters": lang_obj}
199 @openerpweb.jsonrequest
200 def version_info(self, req):
202 "version": web.common.release.version
205 class Database(openerpweb.Controller):
206 _cp_path = "/web/database"
208 @openerpweb.jsonrequest
209 def get_list(self, req):
210 proxy = req.session.proxy("db")
212 h = req.httprequest.headers['Host'].split(':')[0]
214 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
215 dbs = [i for i in dbs if re.match(r, i)]
216 return {"db_list": dbs}
218 @openerpweb.jsonrequest
219 def progress(self, req, password, id):
220 return req.session.proxy('db').get_progress(password, id)
222 @openerpweb.jsonrequest
223 def create(self, req, fields):
225 params = dict(map(operator.itemgetter('name', 'value'), fields))
227 params['super_admin_pwd'],
229 bool(params.get('demo_data')),
231 params['create_admin_pwd']
235 return req.session.proxy("db").create(*create_attrs)
236 except xmlrpclib.Fault, e:
237 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
238 return {'error': e.faultCode, 'title': 'Create Database'}
239 return {'error': 'Could not create database !', 'title': 'Create Database'}
241 @openerpweb.jsonrequest
242 def drop(self, req, fields):
243 password, db = operator.itemgetter(
244 'drop_pwd', 'drop_db')(
245 dict(map(operator.itemgetter('name', 'value'), fields)))
248 return req.session.proxy("db").drop(password, db)
249 except xmlrpclib.Fault, e:
250 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
251 return {'error': e.faultCode, 'title': 'Drop Database'}
252 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
254 @openerpweb.httprequest
255 def backup(self, req, backup_db, backup_pwd, token):
257 db_dump = base64.b64decode(
258 req.session.proxy("db").dump(backup_pwd, backup_db))
259 return req.make_response(db_dump,
260 [('Content-Type', 'application/octet-stream; charset=binary'),
261 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
262 {'fileToken': int(token)}
264 except xmlrpclib.Fault, e:
265 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
266 return 'Backup Database|' + e.faultCode
267 return 'Backup Database|Could not generate database backup'
269 @openerpweb.httprequest
270 def restore(self, req, db_file, restore_pwd, new_db):
272 data = base64.b64encode(db_file.file.read())
273 req.session.proxy("db").restore(restore_pwd, new_db, data)
275 except xmlrpclib.Fault, e:
276 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
277 raise Exception("AccessDenied")
279 @openerpweb.jsonrequest
280 def change_password(self, req, fields):
281 old_password, new_password = operator.itemgetter(
282 'old_pwd', 'new_pwd')(
283 dict(map(operator.itemgetter('name', 'value'), fields)))
285 return req.session.proxy("db").change_admin_password(old_password, new_password)
286 except xmlrpclib.Fault, e:
287 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
288 return {'error': e.faultCode, 'title': 'Change Password'}
289 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
291 class Session(openerpweb.Controller):
292 _cp_path = "/web/session"
294 @openerpweb.jsonrequest
295 def login(self, req, db, login, password):
296 req.session.login(db, login, password)
297 ctx = req.session.get_context() if req.session._uid else {}
300 "session_id": req.session_id,
301 "uid": req.session._uid,
303 "db": req.session._db
306 @openerpweb.jsonrequest
307 def get_session_info(self, req):
308 req.session.assert_valid(force=True)
310 "uid": req.session._uid,
311 "context": req.session.get_context() if req.session._uid else False,
312 "db": req.session._db
315 @openerpweb.jsonrequest
316 def change_password (self,req,fields):
317 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
318 dict(map(operator.itemgetter('name', 'value'), fields)))
319 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
320 return {'error':'All passwords have to be filled.','title': 'Change Password'}
321 if new_password != confirm_password:
322 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
324 if req.session.model('res.users').change_password(
325 old_password, new_password):
326 return {'new_password':new_password}
328 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
329 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
331 @openerpweb.jsonrequest
332 def sc_list(self, req):
333 return req.session.model('ir.ui.view_sc').get_sc(
334 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
336 @openerpweb.jsonrequest
337 def get_lang_list(self, req):
340 'lang_list': (req.session.proxy("db").list_lang() or []),
344 return {"error": e, "title": "Languages"}
346 @openerpweb.jsonrequest
347 def modules(self, req):
348 # Compute available candidates module
349 loadable = openerpweb.addons_manifest.iterkeys()
350 loaded = req.config.server_wide_modules
351 candidates = [mod for mod in loadable if mod not in loaded]
353 # Compute active true modules that might be on the web side only
354 active = set(name for name in candidates
355 if openerpweb.addons_manifest[name].get('active'))
357 # Retrieve database installed modules
358 Modules = req.session.model('ir.module.module')
359 installed = set(module['name'] for module in Modules.search_read(
360 [('state','=','installed'), ('name','in', candidates)], ['name']))
363 return list(active | installed)
365 @openerpweb.jsonrequest
366 def eval_domain_and_context(self, req, contexts, domains,
368 """ Evaluates sequences of domains and contexts, composing them into
369 a single context, domain or group_by sequence.
371 :param list contexts: list of contexts to merge together. Contexts are
372 evaluated in sequence, all previous contexts
373 are part of their own evaluation context
374 (starting at the session context).
375 :param list domains: list of domains to merge together. Domains are
376 evaluated in sequence and appended to one another
377 (implicit AND), their evaluation domain is the
378 result of merging all contexts.
379 :param list group_by_seq: list of domains (which may be in a different
380 order than the ``contexts`` parameter),
381 evaluated in sequence, their ``'group_by'``
382 key is extracted if they have one.
387 the global context created by merging all of
391 the concatenation of all domains
394 a list of fields to group by, potentially empty (in which case
395 no group by should be performed)
397 context, domain = eval_context_and_domain(req.session,
398 web.common.nonliterals.CompoundContext(*(contexts or [])),
399 web.common.nonliterals.CompoundDomain(*(domains or [])))
401 group_by_sequence = []
402 for candidate in (group_by_seq or []):
403 ctx = req.session.eval_context(candidate, context)
404 group_by = ctx.get('group_by')
407 elif isinstance(group_by, basestring):
408 group_by_sequence.append(group_by)
410 group_by_sequence.extend(group_by)
415 'group_by': group_by_sequence
418 @openerpweb.jsonrequest
419 def save_session_action(self, req, the_action):
421 This method store an action object in the session object and returns an integer
422 identifying that action. The method get_session_action() can be used to get
425 :param the_action: The action to save in the session.
426 :type the_action: anything
427 :return: A key identifying the saved action.
430 saved_actions = req.httpsession.get('saved_actions')
431 if not saved_actions:
432 saved_actions = {"next":0, "actions":{}}
433 req.httpsession['saved_actions'] = saved_actions
434 # we don't allow more than 10 stored actions
435 if len(saved_actions["actions"]) >= 10:
436 del saved_actions["actions"][min(saved_actions["actions"].keys())]
437 key = saved_actions["next"]
438 saved_actions["actions"][key] = the_action
439 saved_actions["next"] = key + 1
442 @openerpweb.jsonrequest
443 def get_session_action(self, req, key):
445 Gets back a previously saved action. This method can return None if the action
446 was saved since too much time (this case should be handled in a smart way).
448 :param key: The key given by save_session_action()
450 :return: The saved action or None.
453 saved_actions = req.httpsession.get('saved_actions')
454 if not saved_actions:
456 return saved_actions["actions"].get(key)
458 @openerpweb.jsonrequest
459 def check(self, req):
460 req.session.assert_valid()
463 def eval_context_and_domain(session, context, domain=None):
464 e_context = session.eval_context(context)
465 # should we give the evaluated context as an evaluation context to the domain?
466 e_domain = session.eval_domain(domain or [])
468 return e_context, e_domain
470 def load_actions_from_ir_values(req, key, key2, models, meta):
471 context = req.session.eval_context(req.context)
472 Values = req.session.model('ir.values')
473 actions = Values.get(key, key2, models, meta, context)
475 return [(id, name, clean_action(req, action))
476 for id, name, action in actions]
478 def clean_action(req, action, do_not_eval=False):
479 action.setdefault('flags', {})
481 context = req.session.eval_context(req.context)
482 eval_ctx = req.session.evaluation_context(context)
485 # values come from the server, we can just eval them
486 if isinstance(action.get('context'), basestring):
487 action['context'] = eval( action['context'], eval_ctx ) or {}
489 if isinstance(action.get('domain'), basestring):
490 action['domain'] = eval( action['domain'], eval_ctx ) or []
492 if 'context' in action:
493 action['context'] = parse_context(action['context'], req.session)
494 if 'domain' in action:
495 action['domain'] = parse_domain(action['domain'], req.session)
497 if 'type' not in action:
498 action['type'] = 'ir.actions.act_window_close'
500 if action['type'] == 'ir.actions.act_window':
501 return fix_view_modes(action)
504 # I think generate_views,fix_view_modes should go into js ActionManager
505 def generate_views(action):
507 While the server generates a sequence called "views" computing dependencies
508 between a bunch of stuff for views coming directly from the database
509 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
510 to return custom view dictionaries generated on the fly.
512 In that case, there is no ``views`` key available on the action.
514 Since the web client relies on ``action['views']``, generate it here from
515 ``view_mode`` and ``view_id``.
517 Currently handles two different cases:
519 * no view_id, multiple view_mode
520 * single view_id, single view_mode
522 :param dict action: action descriptor dictionary to generate a views key for
524 view_id = action.get('view_id', False)
525 if isinstance(view_id, (list, tuple)):
528 # providing at least one view mode is a requirement, not an option
529 view_modes = action['view_mode'].split(',')
531 if len(view_modes) > 1:
533 raise ValueError('Non-db action dictionaries should provide '
534 'either multiple view modes or a single view '
535 'mode and an optional view id.\n\n Got view '
536 'modes %r and view id %r for action %r' % (
537 view_modes, view_id, action))
538 action['views'] = [(False, mode) for mode in view_modes]
540 action['views'] = [(view_id, view_modes[0])]
542 def fix_view_modes(action):
543 """ For historical reasons, OpenERP has weird dealings in relation to
544 view_mode and the view_type attribute (on window actions):
546 * one of the view modes is ``tree``, which stands for both list views
548 * the choice is made by checking ``view_type``, which is either
549 ``form`` for a list view or ``tree`` for an actual tree view
551 This methods simply folds the view_type into view_mode by adding a
552 new view mode ``list`` which is the result of the ``tree`` view_mode
553 in conjunction with the ``form`` view_type.
555 TODO: this should go into the doc, some kind of "peculiarities" section
557 :param dict action: an action descriptor
558 :returns: nothing, the action is modified in place
560 if 'views' not in action:
561 generate_views(action)
563 if action.pop('view_type', 'form') != 'form':
567 [id, mode if mode != 'tree' else 'list']
568 for id, mode in action['views']
573 class Menu(openerpweb.Controller):
574 _cp_path = "/web/menu"
576 @openerpweb.jsonrequest
578 return {'data': self.do_load(req)}
580 def do_load(self, req):
581 """ Loads all menu items (all applications and their sub-menus).
583 :param req: A request object, with an OpenERP session attribute
584 :type req: < session -> OpenERPSession >
585 :return: the menu root
586 :rtype: dict('children': menu_nodes)
588 Menus = req.session.model('ir.ui.menu')
589 # menus are loaded fully unlike a regular tree view, cause there are
590 # less than 512 items
591 context = req.session.eval_context(req.context)
592 menu_ids = Menus.search([], 0, False, False, context)
593 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
594 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
595 menu_items.append(menu_root)
597 # make a tree using parent_id
598 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
599 for menu_item in menu_items:
600 if menu_item['parent_id']:
601 parent = menu_item['parent_id'][0]
604 if parent in menu_items_map:
605 menu_items_map[parent].setdefault(
606 'children', []).append(menu_item)
608 # sort by sequence a tree using parent_id
609 for menu_item in menu_items:
610 menu_item.setdefault('children', []).sort(
611 key=lambda x:x["sequence"])
615 @openerpweb.jsonrequest
616 def action(self, req, menu_id):
617 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
618 [('ir.ui.menu', menu_id)], False)
619 return {"action": actions}
621 class DataSet(openerpweb.Controller):
622 _cp_path = "/web/dataset"
624 @openerpweb.jsonrequest
625 def fields(self, req, model):
626 return {'fields': req.session.model(model).fields_get(False,
627 req.session.eval_context(req.context))}
629 @openerpweb.jsonrequest
630 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
631 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
632 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
634 """ Performs a search() followed by a read() (if needed) using the
635 provided search criteria
637 :param req: a JSON-RPC request object
638 :type req: openerpweb.JsonRequest
639 :param str model: the name of the model to search on
640 :param fields: a list of the fields to return in the result records
642 :param int offset: from which index should the results start being returned
643 :param int limit: the maximum number of records to return
644 :param list domain: the search domain for the query
645 :param list sort: sorting directives
646 :returns: A structure (dict) with two keys: ids (all the ids matching
647 the (domain, context) pair) and records (paginated records
648 matching fields selection set)
651 Model = req.session.model(model)
653 context, domain = eval_context_and_domain(
654 req.session, req.context, domain)
656 ids = Model.search(domain, 0, False, sort or False, context)
657 # need to fill the dataset with all ids for the (domain, context) pair,
658 # so search un-paginated and paginate manually before reading
659 paginated_ids = ids[offset:(offset + limit if limit else None)]
660 if fields and fields == ['id']:
661 # shortcut read if we only want the ids
664 'records': map(lambda id: {'id': id}, paginated_ids)
667 records = Model.read(paginated_ids, fields or False, context)
668 records.sort(key=lambda obj: ids.index(obj['id']))
675 @openerpweb.jsonrequest
676 def read(self, req, model, ids, fields=False):
677 return self.do_search_read(req, model, ids, fields)
679 @openerpweb.jsonrequest
680 def get(self, req, model, ids, fields=False):
681 return self.do_get(req, model, ids, fields)
683 def do_get(self, req, model, ids, fields=False):
684 """ Fetches and returns the records of the model ``model`` whose ids
687 The results are in the same order as the inputs, but elements may be
688 missing (if there is no record left for the id)
690 :param req: the JSON-RPC2 request object
691 :type req: openerpweb.JsonRequest
692 :param model: the model to read from
694 :param ids: a list of identifiers
696 :param fields: a list of fields to fetch, ``False`` or empty to fetch
697 all fields in the model
698 :type fields: list | False
699 :returns: a list of records, in the same order as the list of ids
702 Model = req.session.model(model)
703 records = Model.read(ids, fields, req.session.eval_context(req.context))
705 record_map = dict((record['id'], record) for record in records)
707 return [record_map[id] for id in ids if record_map.get(id)]
709 @openerpweb.jsonrequest
710 def load(self, req, model, id, fields):
711 m = req.session.model(model)
713 r = m.read([id], False, req.session.eval_context(req.context))
716 return {'value': value}
718 @openerpweb.jsonrequest
719 def create(self, req, model, data):
720 m = req.session.model(model)
721 r = m.create(data, req.session.eval_context(req.context))
724 @openerpweb.jsonrequest
725 def save(self, req, model, id, data):
726 m = req.session.model(model)
727 r = m.write([id], data, req.session.eval_context(req.context))
730 @openerpweb.jsonrequest
731 def unlink(self, req, model, ids=()):
732 Model = req.session.model(model)
733 return Model.unlink(ids, req.session.eval_context(req.context))
735 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
736 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
737 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
738 c, d = eval_context_and_domain(req.session, context, domain)
739 if domain_id and len(args) - 1 >= domain_id:
741 if context_id and len(args) - 1 >= context_id:
744 for i in xrange(len(args)):
745 if isinstance(args[i], web.common.nonliterals.BaseContext):
746 args[i] = req.session.eval_context(args[i])
747 if isinstance(args[i], web.common.nonliterals.BaseDomain):
748 args[i] = req.session.eval_domain(args[i])
750 return getattr(req.session.model(model), method)(*args)
752 @openerpweb.jsonrequest
753 def call(self, req, model, method, args, domain_id=None, context_id=None):
754 return self.call_common(req, model, method, args, domain_id, context_id)
756 @openerpweb.jsonrequest
757 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
758 action = self.call_common(req, model, method, args, domain_id, context_id)
759 if isinstance(action, dict) and action.get('type') != '':
760 return {'result': clean_action(req, action)}
761 return {'result': False}
763 @openerpweb.jsonrequest
764 def exec_workflow(self, req, model, id, signal):
765 r = req.session.exec_workflow(model, id, signal)
768 @openerpweb.jsonrequest
769 def default_get(self, req, model, fields):
770 Model = req.session.model(model)
771 return Model.default_get(fields, req.session.eval_context(req.context))
773 @openerpweb.jsonrequest
774 def name_search(self, req, model, search_str, domain=[], context={}):
775 m = req.session.model(model)
776 r = m.name_search(search_str+'%', domain, '=ilike', context)
779 class DataGroup(openerpweb.Controller):
780 _cp_path = "/web/group"
781 @openerpweb.jsonrequest
782 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
783 Model = req.session.model(model)
784 context, domain = eval_context_and_domain(req.session, req.context, domain)
786 return Model.read_group(
787 domain or [], fields, group_by_fields, 0, False,
788 dict(context, group_by=group_by_fields), sort or False)
790 class View(openerpweb.Controller):
791 _cp_path = "/web/view"
793 def fields_view_get(self, req, model, view_id, view_type,
794 transform=True, toolbar=False, submenu=False):
795 Model = req.session.model(model)
796 context = req.session.eval_context(req.context)
797 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
798 # todo fme?: check that we should pass the evaluated context here
799 self.process_view(req.session, fvg, context, transform)
800 if toolbar and transform:
801 self.process_toolbar(req, fvg['toolbar'])
804 def process_view(self, session, fvg, context, transform):
805 # depending on how it feels, xmlrpclib.ServerProxy can translate
806 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
807 # enjoy unicode strings which can not be trivially converted to
808 # strings, and it blows up during parsing.
810 # So ensure we fix this retardation by converting view xml back to
812 if isinstance(fvg['arch'], unicode):
813 arch = fvg['arch'].encode('utf-8')
818 evaluation_context = session.evaluation_context(context or {})
819 xml = self.transform_view(arch, session, evaluation_context)
821 xml = ElementTree.fromstring(arch)
822 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
824 for field in fvg['fields'].itervalues():
825 if field.get('views'):
826 for view in field["views"].itervalues():
827 self.process_view(session, view, None, transform)
828 if field.get('domain'):
829 field["domain"] = parse_domain(field["domain"], session)
830 if field.get('context'):
831 field["context"] = parse_context(field["context"], session)
833 def process_toolbar(self, req, toolbar):
835 The toolbar is a mapping of section_key: [action_descriptor]
837 We need to clean all those actions in order to ensure correct
840 for actions in toolbar.itervalues():
841 for action in actions:
842 if 'context' in action:
843 action['context'] = parse_context(
844 action['context'], req.session)
845 if 'domain' in action:
846 action['domain'] = parse_domain(
847 action['domain'], req.session)
849 @openerpweb.jsonrequest
850 def add_custom(self, req, view_id, arch):
851 CustomView = req.session.model('ir.ui.view.custom')
853 'user_id': req.session._uid,
856 }, req.session.eval_context(req.context))
857 return {'result': True}
859 @openerpweb.jsonrequest
860 def undo_custom(self, req, view_id, reset=False):
861 CustomView = req.session.model('ir.ui.view.custom')
862 context = req.session.eval_context(req.context)
863 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
864 0, False, False, context)
867 CustomView.unlink(vcustom, context)
869 CustomView.unlink([vcustom[0]], context)
870 return {'result': True}
871 return {'result': False}
873 def transform_view(self, view_string, session, context=None):
874 # transform nodes on the fly via iterparse, instead of
875 # doing it statically on the parsing result
876 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
878 for event, elem in parser:
882 self.parse_domains_and_contexts(elem, session)
885 def parse_domains_and_contexts(self, elem, session):
886 """ Converts domains and contexts from the view into Python objects,
887 either literals if they can be parsed by literal_eval or a special
888 placeholder object if the domain or context refers to free variables.
890 :param elem: the current node being parsed
891 :type param: xml.etree.ElementTree.Element
892 :param session: OpenERP session object, used to store and retrieve
894 :type session: openerpweb.openerpweb.OpenERPSession
896 for el in ['domain', 'filter_domain']:
897 domain = elem.get(el, '').strip()
899 elem.set(el, parse_domain(domain, session))
900 for el in ['context', 'default_get']:
901 context_string = elem.get(el, '').strip()
903 elem.set(el, parse_context(context_string, session))
905 @openerpweb.jsonrequest
906 def load(self, req, model, view_id, view_type, toolbar=False):
907 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
909 def parse_domain(domain, session):
910 """ Parses an arbitrary string containing a domain, transforms it
911 to either a literal domain or a :class:`web.common.nonliterals.Domain`
913 :param domain: the domain to parse, if the domain is not a string it
914 is assumed to be a literal domain and is returned as-is
915 :param session: Current OpenERP session
916 :type session: openerpweb.openerpweb.OpenERPSession
918 if not isinstance(domain, (str, unicode)):
921 return ast.literal_eval(domain)
924 return web.common.nonliterals.Domain(session, domain)
926 def parse_context(context, session):
927 """ Parses an arbitrary string containing a context, transforms it
928 to either a literal context or a :class:`web.common.nonliterals.Context`
930 :param context: the context to parse, if the context is not a string it
931 is assumed to be a literal domain and is returned as-is
932 :param session: Current OpenERP session
933 :type session: openerpweb.openerpweb.OpenERPSession
935 if not isinstance(context, (str, unicode)):
938 return ast.literal_eval(context)
940 return web.common.nonliterals.Context(session, context)
942 class ListView(View):
943 _cp_path = "/web/listview"
945 def process_colors(self, view, row, context):
946 colors = view['arch']['attrs'].get('colors')
953 for pair in colors.split(';')
954 if eval(pair.split(':')[1], dict(context, **row))
959 elif len(color) == 1:
963 class TreeView(View):
964 _cp_path = "/web/treeview"
966 @openerpweb.jsonrequest
967 def action(self, req, model, id):
968 return load_actions_from_ir_values(
969 req,'action', 'tree_but_open',[(model, id)],
972 class SearchView(View):
973 _cp_path = "/web/searchview"
975 @openerpweb.jsonrequest
976 def load(self, req, model, view_id):
977 fields_view = self.fields_view_get(req, model, view_id, 'search')
978 return {'fields_view': fields_view}
980 @openerpweb.jsonrequest
981 def fields_get(self, req, model):
982 Model = req.session.model(model)
983 fields = Model.fields_get(False, req.session.eval_context(req.context))
984 for field in fields.values():
985 # shouldn't convert the views too?
986 if field.get('domain'):
987 field["domain"] = parse_domain(field["domain"], req.session)
988 if field.get('context'):
989 field["context"] = parse_context(field["context"], req.session)
990 return {'fields': fields}
992 @openerpweb.jsonrequest
993 def get_filters(self, req, model):
994 Model = req.session.model("ir.filters")
995 filters = Model.get_filters(model)
996 for filter in filters:
997 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
998 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1001 @openerpweb.jsonrequest
1002 def save_filter(self, req, model, name, context_to_save, domain):
1003 Model = req.session.model("ir.filters")
1004 ctx = web.common.nonliterals.CompoundContext(context_to_save)
1005 ctx.session = req.session
1006 ctx = ctx.evaluate()
1007 domain = web.common.nonliterals.CompoundDomain(domain)
1008 domain.session = req.session
1009 domain = domain.evaluate()
1010 uid = req.session._uid
1011 context = req.session.eval_context(req.context)
1012 to_return = Model.create_or_replace({"context": ctx,
1020 class Binary(openerpweb.Controller):
1021 _cp_path = "/web/binary"
1023 @openerpweb.httprequest
1024 def image(self, req, model, id, field, **kw):
1025 Model = req.session.model(model)
1026 context = req.session.eval_context(req.context)
1030 res = Model.default_get([field], context).get(field)
1032 res = Model.read([int(id)], [field], context)[0].get(field)
1033 image_data = base64.b64decode(res)
1034 except (TypeError, xmlrpclib.Fault):
1035 image_data = self.placeholder(req)
1036 return req.make_response(image_data, [
1037 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1038 def placeholder(self, req):
1039 addons_path = openerpweb.addons_manifest['web']['addons_path']
1040 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1042 @openerpweb.httprequest
1043 def saveas(self, req, model, id, field, fieldname, **kw):
1044 Model = req.session.model(model)
1045 context = req.session.eval_context(req.context)
1047 res = Model.read([int(id)], [field, fieldname], context)[0]
1049 res = Model.default_get([field, fieldname], context)
1050 filecontent = base64.b64decode(res.get(field, ''))
1052 return req.not_found()
1054 filename = '%s_%s' % (model.replace('.', '_'), id)
1056 filename = res.get(fieldname, '') or filename
1057 return req.make_response(filecontent,
1058 [('Content-Type', 'application/octet-stream'),
1059 ('Content-Disposition', 'attachment; filename=' + filename)])
1061 @openerpweb.httprequest
1062 def upload(self, req, callback, ufile):
1063 # TODO: might be useful to have a configuration flag for max-length file uploads
1065 out = """<script language="javascript" type="text/javascript">
1066 var win = window.top.window,
1068 if (typeof(callback) === 'function') {
1069 callback.apply(this, %s);
1071 win.jQuery('#oe_notification', win.document).notify('create', {
1072 title: "Ajax File Upload",
1073 text: "Could not find callback"
1078 args = [ufile.content_length, ufile.filename,
1079 ufile.content_type, base64.b64encode(data)]
1080 except Exception, e:
1081 args = [False, e.message]
1082 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1084 @openerpweb.httprequest
1085 def upload_attachment(self, req, callback, model, id, ufile):
1086 context = req.session.eval_context(req.context)
1087 Model = req.session.model('ir.attachment')
1089 out = """<script language="javascript" type="text/javascript">
1090 var win = window.top.window,
1092 if (typeof(callback) === 'function') {
1093 callback.call(this, %s);
1096 attachment_id = Model.create({
1097 'name': ufile.filename,
1098 'datas': base64.encodestring(ufile.read()),
1103 'filename': ufile.filename,
1106 except Exception, e:
1107 args = { 'error': e.message }
1108 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1110 class Action(openerpweb.Controller):
1111 _cp_path = "/web/action"
1113 @openerpweb.jsonrequest
1114 def load(self, req, action_id, do_not_eval=False):
1115 Actions = req.session.model('ir.actions.actions')
1117 context = req.session.eval_context(req.context)
1118 action_type = Actions.read([action_id], ['type'], context)
1121 if action_type[0]['type'] == 'ir.actions.report.xml':
1122 ctx.update({'bin_size': True})
1124 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1126 value = clean_action(req, action[0], do_not_eval)
1127 return {'result': value}
1129 @openerpweb.jsonrequest
1130 def run(self, req, action_id):
1131 return clean_action(req, req.session.model('ir.actions.server').run(
1132 [action_id], req.session.eval_context(req.context)))
1135 _cp_path = "/web/export"
1137 @openerpweb.jsonrequest
1138 def formats(self, req):
1139 """ Returns all valid export formats
1141 :returns: for each export format, a pair of identifier and printable name
1142 :rtype: [(str, str)]
1146 for path, controller in openerpweb.controllers_path.iteritems()
1147 if path.startswith(self._cp_path)
1148 if hasattr(controller, 'fmt')
1149 ], key=operator.itemgetter(1))
1151 def fields_get(self, req, model):
1152 Model = req.session.model(model)
1153 fields = Model.fields_get(False, req.session.eval_context(req.context))
1156 @openerpweb.jsonrequest
1157 def get_fields(self, req, model, prefix='', parent_name= '',
1158 import_compat=True, parent_field_type=None):
1160 if import_compat and parent_field_type == "many2one":
1163 fields = self.fields_get(req, model)
1164 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1166 fields_sequence = sorted(fields.iteritems(),
1167 key=lambda field: field[1].get('string', ''))
1170 for field_name, field in fields_sequence:
1171 if import_compat and field.get('readonly'):
1172 # If none of the field's states unsets readonly, skip the field
1173 if all(dict(attrs).get('readonly', True)
1174 for attrs in field.get('states', {}).values()):
1177 id = prefix + (prefix and '/'or '') + field_name
1178 name = parent_name + (parent_name and '/' or '') + field['string']
1179 record = {'id': id, 'string': name,
1180 'value': id, 'children': False,
1181 'field_type': field.get('type'),
1182 'required': field.get('required')}
1183 records.append(record)
1185 if len(name.split('/')) < 3 and 'relation' in field:
1186 ref = field.pop('relation')
1187 record['value'] += '/id'
1188 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1190 if not import_compat or field['type'] == 'one2many':
1191 # m2m field in import_compat is childless
1192 record['children'] = True
1196 @openerpweb.jsonrequest
1197 def namelist(self,req, model, export_id):
1198 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1199 export = req.session.model("ir.exports").read([export_id])[0]
1200 export_fields_list = req.session.model("ir.exports.line").read(
1201 export['export_fields'])
1203 fields_data = self.fields_info(
1204 req, model, map(operator.itemgetter('name'), export_fields_list))
1207 {'name': field['name'], 'label': fields_data[field['name']]}
1208 for field in export_fields_list
1211 def fields_info(self, req, model, export_fields):
1213 fields = self.fields_get(req, model)
1214 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1216 # To make fields retrieval more efficient, fetch all sub-fields of a
1217 # given field at the same time. Because the order in the export list is
1218 # arbitrary, this requires ordering all sub-fields of a given field
1219 # together so they can be fetched at the same time
1221 # Works the following way:
1222 # * sort the list of fields to export, the default sorting order will
1223 # put the field itself (if present, for xmlid) and all of its
1224 # sub-fields right after it
1225 # * then, group on: the first field of the path (which is the same for
1226 # a field and for its subfields and the length of splitting on the
1227 # first '/', which basically means grouping the field on one side and
1228 # all of the subfields on the other. This way, we have the field (for
1229 # the xmlid) with length 1, and all of the subfields with the same
1230 # base but a length "flag" of 2
1231 # * if we have a normal field (length 1), just add it to the info
1232 # mapping (with its string) as-is
1233 # * otherwise, recursively call fields_info via graft_subfields.
1234 # all graft_subfields does is take the result of fields_info (on the
1235 # field's model) and prepend the current base (current field), which
1236 # rebuilds the whole sub-tree for the field
1238 # result: because we're not fetching the fields_get for half the
1239 # database models, fetching a namelist with a dozen fields (including
1240 # relational data) falls from ~6s to ~300ms (on the leads model).
1241 # export lists with no sub-fields (e.g. import_compatible lists with
1242 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1243 # there's a single fields_get to execute)
1244 for (base, length), subfields in itertools.groupby(
1245 sorted(export_fields),
1246 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1247 subfields = list(subfields)
1249 # subfields is a seq of $base/*rest, and not loaded yet
1250 info.update(self.graft_subfields(
1251 req, fields[base]['relation'], base, fields[base]['string'],
1255 info[base] = fields[base]['string']
1259 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1260 export_fields = [field.split('/', 1)[1] for field in fields]
1262 (prefix + '/' + k, prefix_string + '/' + v)
1263 for k, v in self.fields_info(req, model, export_fields).iteritems())
1265 #noinspection PyPropertyDefinition
1267 def content_type(self):
1268 """ Provides the format's content type """
1269 raise NotImplementedError()
1271 def filename(self, base):
1272 """ Creates a valid filename for the format (with extension) from the
1273 provided base name (exension-less)
1275 raise NotImplementedError()
1277 def from_data(self, fields, rows):
1278 """ Conversion method from OpenERP's export data to whatever the
1279 current export class outputs
1281 :params list fields: a list of fields to export
1282 :params list rows: a list of records to export
1286 raise NotImplementedError()
1288 @openerpweb.httprequest
1289 def index(self, req, data, token):
1290 model, fields, ids, domain, import_compat = \
1291 operator.itemgetter('model', 'fields', 'ids', 'domain',
1293 simplejson.loads(data))
1295 context = req.session.eval_context(req.context)
1296 Model = req.session.model(model)
1297 ids = ids or Model.search(domain, context=context)
1299 field_names = map(operator.itemgetter('name'), fields)
1300 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1303 columns_headers = field_names
1305 columns_headers = [val['label'].strip() for val in fields]
1308 return req.make_response(self.from_data(columns_headers, import_data),
1309 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1310 ('Content-Type', self.content_type)],
1311 cookies={'fileToken': int(token)})
1313 class CSVExport(Export):
1314 _cp_path = '/web/export/csv'
1315 fmt = ('csv', 'CSV')
1318 def content_type(self):
1319 return 'text/csv;charset=utf8'
1321 def filename(self, base):
1322 return base + '.csv'
1324 def from_data(self, fields, rows):
1326 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1328 writer.writerow(fields)
1333 if isinstance(d, basestring):
1334 d = d.replace('\n',' ').replace('\t',' ')
1336 d = d.encode('utf-8')
1339 if d is False: d = None
1341 writer.writerow(row)
1348 class ExcelExport(Export):
1349 _cp_path = '/web/export/xls'
1350 fmt = ('xls', 'Excel')
1353 def content_type(self):
1354 return 'application/vnd.ms-excel'
1356 def filename(self, base):
1357 return base + '.xls'
1359 def from_data(self, fields, rows):
1362 workbook = xlwt.Workbook()
1363 worksheet = workbook.add_sheet('Sheet 1')
1365 for i, fieldname in enumerate(fields):
1366 worksheet.write(0, i, str(fieldname))
1367 worksheet.col(i).width = 8000 # around 220 pixels
1369 style = xlwt.easyxf('align: wrap yes')
1371 for row_index, row in enumerate(rows):
1372 for cell_index, cell_value in enumerate(row):
1373 if isinstance(cell_value, basestring):
1374 cell_value = re.sub("\r", " ", cell_value)
1375 worksheet.write(row_index + 1, cell_index, cell_value, style)
1384 class Reports(View):
1385 _cp_path = "/web/report"
1386 POLLING_DELAY = 0.25
1388 'doc': 'application/vnd.ms-word',
1389 'html': 'text/html',
1390 'odt': 'application/vnd.oasis.opendocument.text',
1391 'pdf': 'application/pdf',
1392 'sxw': 'application/vnd.sun.xml.writer',
1393 'xls': 'application/vnd.ms-excel',
1396 @openerpweb.httprequest
1397 def index(self, req, action, token):
1398 action = simplejson.loads(action)
1400 report_srv = req.session.proxy("report")
1401 context = req.session.eval_context(
1402 web.common.nonliterals.CompoundContext(
1403 req.context or {}, action[ "context"]))
1406 report_ids = context["active_ids"]
1407 if 'report_type' in action:
1408 report_data['report_type'] = action['report_type']
1409 if 'datas' in action:
1410 if 'ids' in action['datas']:
1411 report_ids = action['datas'].pop('ids')
1412 report_data.update(action['datas'])
1414 report_id = report_srv.report(
1415 req.session._db, req.session._uid, req.session._password,
1416 action["report_name"], report_ids,
1417 report_data, context)
1419 report_struct = None
1421 report_struct = report_srv.report_get(
1422 req.session._db, req.session._uid, req.session._password, report_id)
1423 if report_struct["state"]:
1426 time.sleep(self.POLLING_DELAY)
1428 report = base64.b64decode(report_struct['result'])
1429 if report_struct.get('code') == 'zlib':
1430 report = zlib.decompress(report)
1431 report_mimetype = self.TYPES_MAPPING.get(
1432 report_struct['format'], 'octet-stream')
1433 return req.make_response(report,
1435 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1436 ('Content-Type', report_mimetype),
1437 ('Content-Length', len(report))],
1438 cookies={'fileToken': int(token)})
1441 _cp_path = "/web/import"
1443 def fields_get(self, req, model):
1444 Model = req.session.model(model)
1445 fields = Model.fields_get(False, req.session.eval_context(req.context))
1448 @openerpweb.httprequest
1449 def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1451 data = list(csv.reader(
1452 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1453 except csv.Error, e:
1455 return '<script>window.top.%s(%s);</script>' % (
1456 jsonp, simplejson.dumps({'error': {
1457 'message': 'Error parsing CSV file: %s' % e,
1458 # decodes each byte to a unicode character, which may or
1459 # may not be printable, but decoding will succeed.
1460 # Otherwise simplejson will try to decode the `str` using
1461 # utf-8, which is very likely to blow up on characters out
1462 # of the ascii range (in range [128, 256))
1463 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1466 return '<script>window.top.%s(%s);</script>' % (
1467 jsonp, simplejson.dumps(
1468 {'records': data[:10]}, encoding=csvcode))
1469 except UnicodeDecodeError:
1470 return '<script>window.top.%s(%s);</script>' % (
1471 jsonp, simplejson.dumps({
1472 'message': u"Failed to decode CSV file using encoding %s, "
1473 u"try switching to a different encoding" % csvcode
1476 @openerpweb.httprequest
1477 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1479 modle_obj = req.session.model(model)
1480 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1481 simplejson.loads(meta))
1484 if not (csvdel and len(csvdel) == 1):
1485 error = u"The CSV delimiter must be a single character"
1487 if not indices and fields:
1488 error = u"You must select at least one field to import"
1491 return '<script>window.top.%s(%s);</script>' % (
1492 jsonp, simplejson.dumps({'error': {'message': error}}))
1494 # skip ignored records
1495 data_record = itertools.islice(
1496 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1499 # if only one index, itemgetter will return an atom rather than a tuple
1500 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1501 else: mapper = operator.itemgetter(*indices)
1506 # decode each data row
1508 [record.decode(csvcode) for record in row]
1509 for row in itertools.imap(mapper, data_record)
1510 # don't insert completely empty rows (can happen due to fields
1511 # filtering in case of e.g. o2m content rows)
1514 except UnicodeDecodeError:
1515 error = u"Failed to decode CSV file using encoding %s" % csvcode
1516 except csv.Error, e:
1517 error = u"Could not process CSV file: %s" % e
1519 # If the file contains nothing,
1521 error = u"File to import is empty"
1523 return '<script>window.top.%s(%s);</script>' % (
1524 jsonp, simplejson.dumps({'error': {'message': error}}))
1527 (code, record, message, _nope) = modle_obj.import_data(
1528 fields, data, 'init', '', False,
1529 req.session.eval_context(req.context))
1530 except xmlrpclib.Fault, e:
1531 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1532 return '<script>window.top.%s(%s);</script>' % (
1533 jsonp, simplejson.dumps({'error':error}))
1536 return '<script>window.top.%s(%s);</script>' % (
1537 jsonp, simplejson.dumps({'success':True}))
1539 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1541 return '<script>window.top.%s(%s);</script>' % (
1542 jsonp, simplejson.dumps({'error': {'message':msg}}))