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):
28 """ Concatenate file content
29 return (concat,timestamp)
30 concat: concatenation of file content
31 timestamp: max(os.path.getmtime of file_list)
35 for fname in file_list:
36 ftime = os.path.getmtime(fname)
37 if ftime > files_timestamp:
38 files_timestamp = ftime
39 files_content.append(open(fname).read())
40 files_concat = "".join(files_content)
41 return files_concat,files_timestamp
43 html_template = """<!DOCTYPE html>
44 <html style="height: 100%%">
46 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
47 <title>OpenERP</title>
48 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
51 <script type="text/javascript">
53 var s = new openerp.init(%(modules)s);
58 <body id="oe" class="openerp"></body>
62 class WebClient(openerpweb.Controller):
63 _cp_path = "/web/webclient"
65 def server_wide_modules(self, req):
66 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
69 def manifest_glob(self, req, addons, key):
71 addons = self.server_wide_modules(req)
73 addons = addons.split(',')
75 manifest = openerpweb.addons_manifest.get(addon, None)
78 # ensure does not ends with /
79 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
80 globlist = manifest.get(key, [])
81 for pattern in globlist:
82 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
83 yield path, path[len(addons_path):]
85 def manifest_list(self, req, mods, extension):
87 path = '/web/webclient/' + extension
89 path += '?mods=' + mods
91 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
93 @openerpweb.jsonrequest
94 def csslist(self, req, mods=None):
95 return self.manifest_list(req, mods, 'css')
97 @openerpweb.jsonrequest
98 def jslist(self, req, mods=None):
99 return self.manifest_list(req, mods, 'js')
101 @openerpweb.httprequest
102 def css(self, req, mods=None):
103 files = [f[0] for f in self.manifest_glob(req, mods, 'css')]
104 content,timestamp = concat_files(files)
105 # TODO use timestamp to set Last mofified date and E-tag
106 return req.make_response(content, [('Content-Type', 'text/css')])
108 @openerpweb.httprequest
109 def js(self, req, mods=None):
110 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
111 content,timestamp = concat_files(files)
112 # TODO use timestamp to set Last mofified date and E-tag
113 return req.make_response(content, [('Content-Type', 'application/javascript')])
115 @openerpweb.httprequest
116 def home(self, req, s_action=None, **kw):
117 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
118 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
120 r = html_template % {
123 'modules': simplejson.dumps(self.server_wide_modules(req)),
124 'init': 'new s.web.WebClient("oe").start();',
128 @openerpweb.jsonrequest
129 def translations(self, req, mods, lang):
130 lang_model = req.session.model('res.lang')
131 ids = lang_model.search([("code", "=", lang)])
133 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
134 "grouping", "decimal_point", "thousands_sep"])
138 if lang.count("_") > 0:
142 langs = lang.split(separator)
143 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
146 for addon_name in mods:
147 transl = {"messages":[]}
148 transs[addon_name] = transl
150 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
151 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
152 if not os.path.exists(f_name):
155 with open(f_name) as t_file:
156 po = babel.messages.pofile.read_po(t_file)
160 if x.id and x.string:
161 transl["messages"].append({'id': x.id, 'string': x.string})
162 return {"modules": transs,
163 "lang_parameters": lang_obj}
165 @openerpweb.jsonrequest
166 def version_info(self, req):
168 "version": web.common.release.version
171 class Database(openerpweb.Controller):
172 _cp_path = "/web/database"
174 @openerpweb.jsonrequest
175 def get_list(self, req):
176 proxy = req.session.proxy("db")
178 h = req.httprequest.headers['Host'].split(':')[0]
180 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
181 dbs = [i for i in dbs if re.match(r, i)]
182 return {"db_list": dbs}
184 @openerpweb.jsonrequest
185 def progress(self, req, password, id):
186 return req.session.proxy('db').get_progress(password, id)
188 @openerpweb.jsonrequest
189 def create(self, req, fields):
191 params = dict(map(operator.itemgetter('name', 'value'), fields))
193 params['super_admin_pwd'],
195 bool(params.get('demo_data')),
197 params['create_admin_pwd']
201 return req.session.proxy("db").create(*create_attrs)
202 except xmlrpclib.Fault, e:
203 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
204 return {'error': e.faultCode, 'title': 'Create Database'}
205 return {'error': 'Could not create database !', 'title': 'Create Database'}
207 @openerpweb.jsonrequest
208 def drop(self, req, fields):
209 password, db = operator.itemgetter(
210 'drop_pwd', 'drop_db')(
211 dict(map(operator.itemgetter('name', 'value'), fields)))
214 return req.session.proxy("db").drop(password, db)
215 except xmlrpclib.Fault, e:
216 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
217 return {'error': e.faultCode, 'title': 'Drop Database'}
218 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
220 @openerpweb.httprequest
221 def backup(self, req, backup_db, backup_pwd, token):
223 db_dump = base64.b64decode(
224 req.session.proxy("db").dump(backup_pwd, backup_db))
225 return req.make_response(db_dump,
226 [('Content-Type', 'application/octet-stream; charset=binary'),
227 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
228 {'fileToken': int(token)}
230 except xmlrpclib.Fault, e:
231 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
232 return 'Backup Database|' + e.faultCode
233 return 'Backup Database|Could not generate database backup'
235 @openerpweb.httprequest
236 def restore(self, req, db_file, restore_pwd, new_db):
238 data = base64.b64encode(db_file.file.read())
239 req.session.proxy("db").restore(restore_pwd, new_db, data)
241 except xmlrpclib.Fault, e:
242 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
243 raise Exception("AccessDenied")
245 @openerpweb.jsonrequest
246 def change_password(self, req, fields):
247 old_password, new_password = operator.itemgetter(
248 'old_pwd', 'new_pwd')(
249 dict(map(operator.itemgetter('name', 'value'), fields)))
251 return req.session.proxy("db").change_admin_password(old_password, new_password)
252 except xmlrpclib.Fault, e:
253 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
254 return {'error': e.faultCode, 'title': 'Change Password'}
255 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
257 class Session(openerpweb.Controller):
258 _cp_path = "/web/session"
260 @openerpweb.jsonrequest
261 def login(self, req, db, login, password):
262 req.session.login(db, login, password)
263 ctx = req.session.get_context() if req.session._uid else {}
266 "session_id": req.session_id,
267 "uid": req.session._uid,
269 "db": req.session._db
272 @openerpweb.jsonrequest
273 def get_session_info(self, req):
274 req.session.assert_valid(force=True)
276 "uid": req.session._uid,
277 "context": req.session.get_context() if req.session._uid else False,
278 "db": req.session._db
281 @openerpweb.jsonrequest
282 def change_password (self,req,fields):
283 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
284 dict(map(operator.itemgetter('name', 'value'), fields)))
285 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
286 return {'error':'All passwords have to be filled.','title': 'Change Password'}
287 if new_password != confirm_password:
288 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
290 if req.session.model('res.users').change_password(
291 old_password, new_password):
292 return {'new_password':new_password}
294 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
295 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
297 @openerpweb.jsonrequest
298 def sc_list(self, req):
299 return req.session.model('ir.ui.view_sc').get_sc(
300 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
302 @openerpweb.jsonrequest
303 def get_lang_list(self, req):
306 'lang_list': (req.session.proxy("db").list_lang() or []),
310 return {"error": e, "title": "Languages"}
312 @openerpweb.jsonrequest
313 def modules(self, req):
314 # Compute available candidates module
315 loadable = openerpweb.addons_manifest.iterkeys()
316 loaded = req.config.server_wide_modules
317 candidates = [mod for mod in loadable if mod not in loaded]
319 # Compute active true modules that might be on the web side only
320 active = set(name for name in candidates
321 if openerpweb.addons_manifest[name].get('active'))
323 # Retrieve database installed modules
324 Modules = req.session.model('ir.module.module')
325 installed = set(module['name'] for module in Modules.search_read(
326 [('state','=','installed'), ('name','in', candidates)], ['name']))
329 return list(active | installed)
331 @openerpweb.jsonrequest
332 def eval_domain_and_context(self, req, contexts, domains,
334 """ Evaluates sequences of domains and contexts, composing them into
335 a single context, domain or group_by sequence.
337 :param list contexts: list of contexts to merge together. Contexts are
338 evaluated in sequence, all previous contexts
339 are part of their own evaluation context
340 (starting at the session context).
341 :param list domains: list of domains to merge together. Domains are
342 evaluated in sequence and appended to one another
343 (implicit AND), their evaluation domain is the
344 result of merging all contexts.
345 :param list group_by_seq: list of domains (which may be in a different
346 order than the ``contexts`` parameter),
347 evaluated in sequence, their ``'group_by'``
348 key is extracted if they have one.
353 the global context created by merging all of
357 the concatenation of all domains
360 a list of fields to group by, potentially empty (in which case
361 no group by should be performed)
363 context, domain = eval_context_and_domain(req.session,
364 web.common.nonliterals.CompoundContext(*(contexts or [])),
365 web.common.nonliterals.CompoundDomain(*(domains or [])))
367 group_by_sequence = []
368 for candidate in (group_by_seq or []):
369 ctx = req.session.eval_context(candidate, context)
370 group_by = ctx.get('group_by')
373 elif isinstance(group_by, basestring):
374 group_by_sequence.append(group_by)
376 group_by_sequence.extend(group_by)
381 'group_by': group_by_sequence
384 @openerpweb.jsonrequest
385 def save_session_action(self, req, the_action):
387 This method store an action object in the session object and returns an integer
388 identifying that action. The method get_session_action() can be used to get
391 :param the_action: The action to save in the session.
392 :type the_action: anything
393 :return: A key identifying the saved action.
396 saved_actions = req.httpsession.get('saved_actions')
397 if not saved_actions:
398 saved_actions = {"next":0, "actions":{}}
399 req.httpsession['saved_actions'] = saved_actions
400 # we don't allow more than 10 stored actions
401 if len(saved_actions["actions"]) >= 10:
402 del saved_actions["actions"][min(saved_actions["actions"].keys())]
403 key = saved_actions["next"]
404 saved_actions["actions"][key] = the_action
405 saved_actions["next"] = key + 1
408 @openerpweb.jsonrequest
409 def get_session_action(self, req, key):
411 Gets back a previously saved action. This method can return None if the action
412 was saved since too much time (this case should be handled in a smart way).
414 :param key: The key given by save_session_action()
416 :return: The saved action or None.
419 saved_actions = req.httpsession.get('saved_actions')
420 if not saved_actions:
422 return saved_actions["actions"].get(key)
424 @openerpweb.jsonrequest
425 def check(self, req):
426 req.session.assert_valid()
429 def eval_context_and_domain(session, context, domain=None):
430 e_context = session.eval_context(context)
431 # should we give the evaluated context as an evaluation context to the domain?
432 e_domain = session.eval_domain(domain or [])
434 return e_context, e_domain
436 def load_actions_from_ir_values(req, key, key2, models, meta):
437 context = req.session.eval_context(req.context)
438 Values = req.session.model('ir.values')
439 actions = Values.get(key, key2, models, meta, context)
441 return [(id, name, clean_action(req, action))
442 for id, name, action in actions]
444 def clean_action(req, action, do_not_eval=False):
445 action.setdefault('flags', {})
447 context = req.session.eval_context(req.context)
448 eval_ctx = req.session.evaluation_context(context)
451 # values come from the server, we can just eval them
452 if isinstance(action.get('context'), basestring):
453 action['context'] = eval( action['context'], eval_ctx ) or {}
455 if isinstance(action.get('domain'), basestring):
456 action['domain'] = eval( action['domain'], eval_ctx ) or []
458 if 'context' in action:
459 action['context'] = parse_context(action['context'], req.session)
460 if 'domain' in action:
461 action['domain'] = parse_domain(action['domain'], req.session)
463 if 'type' not in action:
464 action['type'] = 'ir.actions.act_window_close'
466 if action['type'] == 'ir.actions.act_window':
467 return fix_view_modes(action)
470 # I think generate_views,fix_view_modes should go into js ActionManager
471 def generate_views(action):
473 While the server generates a sequence called "views" computing dependencies
474 between a bunch of stuff for views coming directly from the database
475 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
476 to return custom view dictionaries generated on the fly.
478 In that case, there is no ``views`` key available on the action.
480 Since the web client relies on ``action['views']``, generate it here from
481 ``view_mode`` and ``view_id``.
483 Currently handles two different cases:
485 * no view_id, multiple view_mode
486 * single view_id, single view_mode
488 :param dict action: action descriptor dictionary to generate a views key for
490 view_id = action.get('view_id', False)
491 if isinstance(view_id, (list, tuple)):
494 # providing at least one view mode is a requirement, not an option
495 view_modes = action['view_mode'].split(',')
497 if len(view_modes) > 1:
499 raise ValueError('Non-db action dictionaries should provide '
500 'either multiple view modes or a single view '
501 'mode and an optional view id.\n\n Got view '
502 'modes %r and view id %r for action %r' % (
503 view_modes, view_id, action))
504 action['views'] = [(False, mode) for mode in view_modes]
506 action['views'] = [(view_id, view_modes[0])]
508 def fix_view_modes(action):
509 """ For historical reasons, OpenERP has weird dealings in relation to
510 view_mode and the view_type attribute (on window actions):
512 * one of the view modes is ``tree``, which stands for both list views
514 * the choice is made by checking ``view_type``, which is either
515 ``form`` for a list view or ``tree`` for an actual tree view
517 This methods simply folds the view_type into view_mode by adding a
518 new view mode ``list`` which is the result of the ``tree`` view_mode
519 in conjunction with the ``form`` view_type.
521 TODO: this should go into the doc, some kind of "peculiarities" section
523 :param dict action: an action descriptor
524 :returns: nothing, the action is modified in place
526 if 'views' not in action:
527 generate_views(action)
529 if action.pop('view_type') != 'form':
533 [id, mode if mode != 'tree' else 'list']
534 for id, mode in action['views']
539 class Menu(openerpweb.Controller):
540 _cp_path = "/web/menu"
542 @openerpweb.jsonrequest
544 return {'data': self.do_load(req)}
546 def do_load(self, req):
547 """ Loads all menu items (all applications and their sub-menus).
549 :param req: A request object, with an OpenERP session attribute
550 :type req: < session -> OpenERPSession >
551 :return: the menu root
552 :rtype: dict('children': menu_nodes)
554 Menus = req.session.model('ir.ui.menu')
555 # menus are loaded fully unlike a regular tree view, cause there are
556 # less than 512 items
557 context = req.session.eval_context(req.context)
558 menu_ids = Menus.search([], 0, False, False, context)
559 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
560 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
561 menu_items.append(menu_root)
563 # make a tree using parent_id
564 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
565 for menu_item in menu_items:
566 if menu_item['parent_id']:
567 parent = menu_item['parent_id'][0]
570 if parent in menu_items_map:
571 menu_items_map[parent].setdefault(
572 'children', []).append(menu_item)
574 # sort by sequence a tree using parent_id
575 for menu_item in menu_items:
576 menu_item.setdefault('children', []).sort(
577 key=lambda x:x["sequence"])
581 @openerpweb.jsonrequest
582 def action(self, req, menu_id):
583 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
584 [('ir.ui.menu', menu_id)], False)
585 return {"action": actions}
587 class DataSet(openerpweb.Controller):
588 _cp_path = "/web/dataset"
590 @openerpweb.jsonrequest
591 def fields(self, req, model):
592 return {'fields': req.session.model(model).fields_get(False,
593 req.session.eval_context(req.context))}
595 @openerpweb.jsonrequest
596 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
597 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
598 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
600 """ Performs a search() followed by a read() (if needed) using the
601 provided search criteria
603 :param req: a JSON-RPC request object
604 :type req: openerpweb.JsonRequest
605 :param str model: the name of the model to search on
606 :param fields: a list of the fields to return in the result records
608 :param int offset: from which index should the results start being returned
609 :param int limit: the maximum number of records to return
610 :param list domain: the search domain for the query
611 :param list sort: sorting directives
612 :returns: A structure (dict) with two keys: ids (all the ids matching
613 the (domain, context) pair) and records (paginated records
614 matching fields selection set)
617 Model = req.session.model(model)
619 context, domain = eval_context_and_domain(
620 req.session, req.context, domain)
622 ids = Model.search(domain, 0, False, sort or False, context)
623 # need to fill the dataset with all ids for the (domain, context) pair,
624 # so search un-paginated and paginate manually before reading
625 paginated_ids = ids[offset:(offset + limit if limit else None)]
626 if fields and fields == ['id']:
627 # shortcut read if we only want the ids
630 'records': map(lambda id: {'id': id}, paginated_ids)
633 records = Model.read(paginated_ids, fields or False, context)
634 records.sort(key=lambda obj: ids.index(obj['id']))
641 @openerpweb.jsonrequest
642 def read(self, req, model, ids, fields=False):
643 return self.do_search_read(req, model, ids, fields)
645 @openerpweb.jsonrequest
646 def get(self, req, model, ids, fields=False):
647 return self.do_get(req, model, ids, fields)
649 def do_get(self, req, model, ids, fields=False):
650 """ Fetches and returns the records of the model ``model`` whose ids
653 The results are in the same order as the inputs, but elements may be
654 missing (if there is no record left for the id)
656 :param req: the JSON-RPC2 request object
657 :type req: openerpweb.JsonRequest
658 :param model: the model to read from
660 :param ids: a list of identifiers
662 :param fields: a list of fields to fetch, ``False`` or empty to fetch
663 all fields in the model
664 :type fields: list | False
665 :returns: a list of records, in the same order as the list of ids
668 Model = req.session.model(model)
669 records = Model.read(ids, fields, req.session.eval_context(req.context))
671 record_map = dict((record['id'], record) for record in records)
673 return [record_map[id] for id in ids if record_map.get(id)]
675 @openerpweb.jsonrequest
676 def load(self, req, model, id, fields):
677 m = req.session.model(model)
679 r = m.read([id], False, req.session.eval_context(req.context))
682 return {'value': value}
684 @openerpweb.jsonrequest
685 def create(self, req, model, data):
686 m = req.session.model(model)
687 r = m.create(data, req.session.eval_context(req.context))
690 @openerpweb.jsonrequest
691 def save(self, req, model, id, data):
692 m = req.session.model(model)
693 r = m.write([id], data, req.session.eval_context(req.context))
696 @openerpweb.jsonrequest
697 def unlink(self, req, model, ids=()):
698 Model = req.session.model(model)
699 return Model.unlink(ids, req.session.eval_context(req.context))
701 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
702 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
703 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
704 c, d = eval_context_and_domain(req.session, context, domain)
705 if domain_id and len(args) - 1 >= domain_id:
707 if context_id and len(args) - 1 >= context_id:
710 for i in xrange(len(args)):
711 if isinstance(args[i], web.common.nonliterals.BaseContext):
712 args[i] = req.session.eval_context(args[i])
713 if isinstance(args[i], web.common.nonliterals.BaseDomain):
714 args[i] = req.session.eval_domain(args[i])
716 return getattr(req.session.model(model), method)(*args)
718 @openerpweb.jsonrequest
719 def call(self, req, model, method, args, domain_id=None, context_id=None):
720 return self.call_common(req, model, method, args, domain_id, context_id)
722 @openerpweb.jsonrequest
723 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
724 action = self.call_common(req, model, method, args, domain_id, context_id)
725 if isinstance(action, dict) and action.get('type') != '':
726 return {'result': clean_action(req, action)}
727 return {'result': False}
729 @openerpweb.jsonrequest
730 def exec_workflow(self, req, model, id, signal):
731 r = req.session.exec_workflow(model, id, signal)
734 @openerpweb.jsonrequest
735 def default_get(self, req, model, fields):
736 Model = req.session.model(model)
737 return Model.default_get(fields, req.session.eval_context(req.context))
739 @openerpweb.jsonrequest
740 def name_search(self, req, model, search_str, domain=[], context={}):
741 m = req.session.model(model)
742 r = m.name_search(search_str+'%', domain, '=ilike', context)
745 class DataGroup(openerpweb.Controller):
746 _cp_path = "/web/group"
747 @openerpweb.jsonrequest
748 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
749 Model = req.session.model(model)
750 context, domain = eval_context_and_domain(req.session, req.context, domain)
752 return Model.read_group(
753 domain or [], fields, group_by_fields, 0, False,
754 dict(context, group_by=group_by_fields), sort or False)
756 class View(openerpweb.Controller):
757 _cp_path = "/web/view"
759 def fields_view_get(self, req, model, view_id, view_type,
760 transform=True, toolbar=False, submenu=False):
761 Model = req.session.model(model)
762 context = req.session.eval_context(req.context)
763 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
764 # todo fme?: check that we should pass the evaluated context here
765 self.process_view(req.session, fvg, context, transform)
766 if toolbar and transform:
767 self.process_toolbar(req, fvg['toolbar'])
770 def process_view(self, session, fvg, context, transform):
771 # depending on how it feels, xmlrpclib.ServerProxy can translate
772 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
773 # enjoy unicode strings which can not be trivially converted to
774 # strings, and it blows up during parsing.
776 # So ensure we fix this retardation by converting view xml back to
778 if isinstance(fvg['arch'], unicode):
779 arch = fvg['arch'].encode('utf-8')
784 evaluation_context = session.evaluation_context(context or {})
785 xml = self.transform_view(arch, session, evaluation_context)
787 xml = ElementTree.fromstring(arch)
788 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
790 for field in fvg['fields'].itervalues():
791 if field.get('views'):
792 for view in field["views"].itervalues():
793 self.process_view(session, view, None, transform)
794 if field.get('domain'):
795 field["domain"] = parse_domain(field["domain"], session)
796 if field.get('context'):
797 field["context"] = parse_context(field["context"], session)
799 def process_toolbar(self, req, toolbar):
801 The toolbar is a mapping of section_key: [action_descriptor]
803 We need to clean all those actions in order to ensure correct
806 for actions in toolbar.itervalues():
807 for action in actions:
808 if 'context' in action:
809 action['context'] = parse_context(
810 action['context'], req.session)
811 if 'domain' in action:
812 action['domain'] = parse_domain(
813 action['domain'], req.session)
815 @openerpweb.jsonrequest
816 def add_custom(self, req, view_id, arch):
817 CustomView = req.session.model('ir.ui.view.custom')
819 'user_id': req.session._uid,
822 }, req.session.eval_context(req.context))
823 return {'result': True}
825 @openerpweb.jsonrequest
826 def undo_custom(self, req, view_id, reset=False):
827 CustomView = req.session.model('ir.ui.view.custom')
828 context = req.session.eval_context(req.context)
829 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
830 0, False, False, context)
833 CustomView.unlink(vcustom, context)
835 CustomView.unlink([vcustom[0]], context)
836 return {'result': True}
837 return {'result': False}
839 def transform_view(self, view_string, session, context=None):
840 # transform nodes on the fly via iterparse, instead of
841 # doing it statically on the parsing result
842 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
844 for event, elem in parser:
848 self.parse_domains_and_contexts(elem, session)
851 def parse_domains_and_contexts(self, elem, session):
852 """ Converts domains and contexts from the view into Python objects,
853 either literals if they can be parsed by literal_eval or a special
854 placeholder object if the domain or context refers to free variables.
856 :param elem: the current node being parsed
857 :type param: xml.etree.ElementTree.Element
858 :param session: OpenERP session object, used to store and retrieve
860 :type session: openerpweb.openerpweb.OpenERPSession
862 for el in ['domain', 'filter_domain']:
863 domain = elem.get(el, '').strip()
865 elem.set(el, parse_domain(domain, session))
866 for el in ['context', 'default_get']:
867 context_string = elem.get(el, '').strip()
869 elem.set(el, parse_context(context_string, session))
871 @openerpweb.jsonrequest
872 def load(self, req, model, view_id, view_type, toolbar=False):
873 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
875 def parse_domain(domain, session):
876 """ Parses an arbitrary string containing a domain, transforms it
877 to either a literal domain or a :class:`web.common.nonliterals.Domain`
879 :param domain: the domain to parse, if the domain is not a string it
880 is assumed to be a literal domain and is returned as-is
881 :param session: Current OpenERP session
882 :type session: openerpweb.openerpweb.OpenERPSession
884 if not isinstance(domain, (str, unicode)):
887 return ast.literal_eval(domain)
890 return web.common.nonliterals.Domain(session, domain)
892 def parse_context(context, session):
893 """ Parses an arbitrary string containing a context, transforms it
894 to either a literal context or a :class:`web.common.nonliterals.Context`
896 :param context: the context to parse, if the context is not a string it
897 is assumed to be a literal domain and is returned as-is
898 :param session: Current OpenERP session
899 :type session: openerpweb.openerpweb.OpenERPSession
901 if not isinstance(context, (str, unicode)):
904 return ast.literal_eval(context)
906 return web.common.nonliterals.Context(session, context)
908 class ListView(View):
909 _cp_path = "/web/listview"
911 def process_colors(self, view, row, context):
912 colors = view['arch']['attrs'].get('colors')
919 for pair in colors.split(';')
920 if eval(pair.split(':')[1], dict(context, **row))
925 elif len(color) == 1:
929 class TreeView(View):
930 _cp_path = "/web/treeview"
932 @openerpweb.jsonrequest
933 def action(self, req, model, id):
934 return load_actions_from_ir_values(
935 req,'action', 'tree_but_open',[(model, id)],
938 class SearchView(View):
939 _cp_path = "/web/searchview"
941 @openerpweb.jsonrequest
942 def load(self, req, model, view_id):
943 fields_view = self.fields_view_get(req, model, view_id, 'search')
944 return {'fields_view': fields_view}
946 @openerpweb.jsonrequest
947 def fields_get(self, req, model):
948 Model = req.session.model(model)
949 fields = Model.fields_get(False, req.session.eval_context(req.context))
950 for field in fields.values():
951 # shouldn't convert the views too?
952 if field.get('domain'):
953 field["domain"] = parse_domain(field["domain"], req.session)
954 if field.get('context'):
955 field["context"] = parse_context(field["context"], req.session)
956 return {'fields': fields}
958 @openerpweb.jsonrequest
959 def get_filters(self, req, model):
960 Model = req.session.model("ir.filters")
961 filters = Model.get_filters(model)
962 for filter in filters:
963 filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
964 filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
967 @openerpweb.jsonrequest
968 def save_filter(self, req, model, name, context_to_save, domain):
969 Model = req.session.model("ir.filters")
970 ctx = web.common.nonliterals.CompoundContext(context_to_save)
971 ctx.session = req.session
973 domain = web.common.nonliterals.CompoundDomain(domain)
974 domain.session = req.session
975 domain = domain.evaluate()
976 uid = req.session._uid
977 context = req.session.eval_context(req.context)
978 to_return = Model.create_or_replace({"context": ctx,
986 class Binary(openerpweb.Controller):
987 _cp_path = "/web/binary"
989 @openerpweb.httprequest
990 def image(self, req, model, id, field, **kw):
991 Model = req.session.model(model)
992 context = req.session.eval_context(req.context)
996 res = Model.default_get([field], context).get(field)
998 res = Model.read([int(id)], [field], context)[0].get(field)
999 image_data = base64.b64decode(res)
1000 except (TypeError, xmlrpclib.Fault):
1001 image_data = self.placeholder(req)
1002 return req.make_response(image_data, [
1003 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1004 def placeholder(self, req):
1005 addons_path = openerpweb.addons_manifest['web']['addons_path']
1006 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1008 @openerpweb.httprequest
1009 def saveas(self, req, model, id, field, fieldname, **kw):
1010 Model = req.session.model(model)
1011 context = req.session.eval_context(req.context)
1013 res = Model.read([int(id)], [field, fieldname], context)[0]
1015 res = Model.default_get([field, fieldname], context)
1016 filecontent = base64.b64decode(res.get(field, ''))
1018 return req.not_found()
1020 filename = '%s_%s' % (model.replace('.', '_'), id)
1022 filename = res.get(fieldname, '') or filename
1023 return req.make_response(filecontent,
1024 [('Content-Type', 'application/octet-stream'),
1025 ('Content-Disposition', 'attachment; filename=' + filename)])
1027 @openerpweb.httprequest
1028 def upload(self, req, callback, ufile):
1029 # TODO: might be useful to have a configuration flag for max-length file uploads
1031 out = """<script language="javascript" type="text/javascript">
1032 var win = window.top.window,
1034 if (typeof(callback) === 'function') {
1035 callback.apply(this, %s);
1037 win.jQuery('#oe_notification', win.document).notify('create', {
1038 title: "Ajax File Upload",
1039 text: "Could not find callback"
1044 args = [ufile.content_length, ufile.filename,
1045 ufile.content_type, base64.b64encode(data)]
1046 except Exception, e:
1047 args = [False, e.message]
1048 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1050 @openerpweb.httprequest
1051 def upload_attachment(self, req, callback, model, id, ufile):
1052 context = req.session.eval_context(req.context)
1053 Model = req.session.model('ir.attachment')
1055 out = """<script language="javascript" type="text/javascript">
1056 var win = window.top.window,
1058 if (typeof(callback) === 'function') {
1059 callback.call(this, %s);
1062 attachment_id = Model.create({
1063 'name': ufile.filename,
1064 'datas': base64.encodestring(ufile.read()),
1069 'filename': ufile.filename,
1072 except Exception, e:
1073 args = { 'error': e.message }
1074 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1076 class Action(openerpweb.Controller):
1077 _cp_path = "/web/action"
1079 @openerpweb.jsonrequest
1080 def load(self, req, action_id, do_not_eval=False):
1081 Actions = req.session.model('ir.actions.actions')
1083 context = req.session.eval_context(req.context)
1084 action_type = Actions.read([action_id], ['type'], context)
1087 if action_type[0]['type'] == 'ir.actions.report.xml':
1088 ctx.update({'bin_size': True})
1090 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1092 value = clean_action(req, action[0], do_not_eval)
1093 return {'result': value}
1095 @openerpweb.jsonrequest
1096 def run(self, req, action_id):
1097 return clean_action(req, req.session.model('ir.actions.server').run(
1098 [action_id], req.session.eval_context(req.context)))
1101 _cp_path = "/web/export"
1103 @openerpweb.jsonrequest
1104 def formats(self, req):
1105 """ Returns all valid export formats
1107 :returns: for each export format, a pair of identifier and printable name
1108 :rtype: [(str, str)]
1112 for path, controller in openerpweb.controllers_path.iteritems()
1113 if path.startswith(self._cp_path)
1114 if hasattr(controller, 'fmt')
1115 ], key=operator.itemgetter(1))
1117 def fields_get(self, req, model):
1118 Model = req.session.model(model)
1119 fields = Model.fields_get(False, req.session.eval_context(req.context))
1122 @openerpweb.jsonrequest
1123 def get_fields(self, req, model, prefix='', parent_name= '',
1124 import_compat=True, parent_field_type=None):
1126 if import_compat and parent_field_type == "many2one":
1129 fields = self.fields_get(req, model)
1130 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1132 fields_sequence = sorted(fields.iteritems(),
1133 key=lambda field: field[1].get('string', ''))
1136 for field_name, field in fields_sequence:
1137 if import_compat and field.get('readonly'):
1138 # If none of the field's states unsets readonly, skip the field
1139 if all(dict(attrs).get('readonly', True)
1140 for attrs in field.get('states', {}).values()):
1143 id = prefix + (prefix and '/'or '') + field_name
1144 name = parent_name + (parent_name and '/' or '') + field['string']
1145 record = {'id': id, 'string': name,
1146 'value': id, 'children': False,
1147 'field_type': field.get('type'),
1148 'required': field.get('required')}
1149 records.append(record)
1151 if len(name.split('/')) < 3 and 'relation' in field:
1152 ref = field.pop('relation')
1153 record['value'] += '/id'
1154 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1156 if not import_compat or field['type'] == 'one2many':
1157 # m2m field in import_compat is childless
1158 record['children'] = True
1162 @openerpweb.jsonrequest
1163 def namelist(self,req, model, export_id):
1164 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1165 export = req.session.model("ir.exports").read([export_id])[0]
1166 export_fields_list = req.session.model("ir.exports.line").read(
1167 export['export_fields'])
1169 fields_data = self.fields_info(
1170 req, model, map(operator.itemgetter('name'), export_fields_list))
1173 {'name': field['name'], 'label': fields_data[field['name']]}
1174 for field in export_fields_list
1177 def fields_info(self, req, model, export_fields):
1179 fields = self.fields_get(req, model)
1180 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1182 # To make fields retrieval more efficient, fetch all sub-fields of a
1183 # given field at the same time. Because the order in the export list is
1184 # arbitrary, this requires ordering all sub-fields of a given field
1185 # together so they can be fetched at the same time
1187 # Works the following way:
1188 # * sort the list of fields to export, the default sorting order will
1189 # put the field itself (if present, for xmlid) and all of its
1190 # sub-fields right after it
1191 # * then, group on: the first field of the path (which is the same for
1192 # a field and for its subfields and the length of splitting on the
1193 # first '/', which basically means grouping the field on one side and
1194 # all of the subfields on the other. This way, we have the field (for
1195 # the xmlid) with length 1, and all of the subfields with the same
1196 # base but a length "flag" of 2
1197 # * if we have a normal field (length 1), just add it to the info
1198 # mapping (with its string) as-is
1199 # * otherwise, recursively call fields_info via graft_subfields.
1200 # all graft_subfields does is take the result of fields_info (on the
1201 # field's model) and prepend the current base (current field), which
1202 # rebuilds the whole sub-tree for the field
1204 # result: because we're not fetching the fields_get for half the
1205 # database models, fetching a namelist with a dozen fields (including
1206 # relational data) falls from ~6s to ~300ms (on the leads model).
1207 # export lists with no sub-fields (e.g. import_compatible lists with
1208 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1209 # there's a single fields_get to execute)
1210 for (base, length), subfields in itertools.groupby(
1211 sorted(export_fields),
1212 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1213 subfields = list(subfields)
1215 # subfields is a seq of $base/*rest, and not loaded yet
1216 info.update(self.graft_subfields(
1217 req, fields[base]['relation'], base, fields[base]['string'],
1221 info[base] = fields[base]['string']
1225 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1226 export_fields = [field.split('/', 1)[1] for field in fields]
1228 (prefix + '/' + k, prefix_string + '/' + v)
1229 for k, v in self.fields_info(req, model, export_fields).iteritems())
1231 #noinspection PyPropertyDefinition
1233 def content_type(self):
1234 """ Provides the format's content type """
1235 raise NotImplementedError()
1237 def filename(self, base):
1238 """ Creates a valid filename for the format (with extension) from the
1239 provided base name (exension-less)
1241 raise NotImplementedError()
1243 def from_data(self, fields, rows):
1244 """ Conversion method from OpenERP's export data to whatever the
1245 current export class outputs
1247 :params list fields: a list of fields to export
1248 :params list rows: a list of records to export
1252 raise NotImplementedError()
1254 @openerpweb.httprequest
1255 def index(self, req, data, token):
1256 model, fields, ids, domain, import_compat = \
1257 operator.itemgetter('model', 'fields', 'ids', 'domain',
1259 simplejson.loads(data))
1261 context = req.session.eval_context(req.context)
1262 Model = req.session.model(model)
1263 ids = ids or Model.search(domain, context=context)
1265 field_names = map(operator.itemgetter('name'), fields)
1266 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1269 columns_headers = field_names
1271 columns_headers = [val['label'].strip() for val in fields]
1274 return req.make_response(self.from_data(columns_headers, import_data),
1275 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1276 ('Content-Type', self.content_type)],
1277 cookies={'fileToken': int(token)})
1279 class CSVExport(Export):
1280 _cp_path = '/web/export/csv'
1281 fmt = ('csv', 'CSV')
1284 def content_type(self):
1285 return 'text/csv;charset=utf8'
1287 def filename(self, base):
1288 return base + '.csv'
1290 def from_data(self, fields, rows):
1292 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1294 writer.writerow(fields)
1299 if isinstance(d, basestring):
1300 d = d.replace('\n',' ').replace('\t',' ')
1302 d = d.encode('utf-8')
1305 if d is False: d = None
1307 writer.writerow(row)
1314 class ExcelExport(Export):
1315 _cp_path = '/web/export/xls'
1316 fmt = ('xls', 'Excel')
1319 def content_type(self):
1320 return 'application/vnd.ms-excel'
1322 def filename(self, base):
1323 return base + '.xls'
1325 def from_data(self, fields, rows):
1328 workbook = xlwt.Workbook()
1329 worksheet = workbook.add_sheet('Sheet 1')
1331 for i, fieldname in enumerate(fields):
1332 worksheet.write(0, i, str(fieldname))
1333 worksheet.col(i).width = 8000 # around 220 pixels
1335 style = xlwt.easyxf('align: wrap yes')
1337 for row_index, row in enumerate(rows):
1338 for cell_index, cell_value in enumerate(row):
1339 if isinstance(cell_value, basestring):
1340 cell_value = re.sub("\r", " ", cell_value)
1341 worksheet.write(row_index + 1, cell_index, cell_value, style)
1350 class Reports(View):
1351 _cp_path = "/web/report"
1352 POLLING_DELAY = 0.25
1354 'doc': 'application/vnd.ms-word',
1355 'html': 'text/html',
1356 'odt': 'application/vnd.oasis.opendocument.text',
1357 'pdf': 'application/pdf',
1358 'sxw': 'application/vnd.sun.xml.writer',
1359 'xls': 'application/vnd.ms-excel',
1362 @openerpweb.httprequest
1363 def index(self, req, action, token):
1364 action = simplejson.loads(action)
1366 report_srv = req.session.proxy("report")
1367 context = req.session.eval_context(
1368 web.common.nonliterals.CompoundContext(
1369 req.context or {}, action[ "context"]))
1372 report_ids = context["active_ids"]
1373 if 'report_type' in action:
1374 report_data['report_type'] = action['report_type']
1375 if 'datas' in action:
1376 if 'form' in action['datas']:
1377 report_data['form'] = action['datas']['form']
1378 if 'ids' in action['datas']:
1379 report_ids = action['datas']['ids']
1381 report_id = report_srv.report(
1382 req.session._db, req.session._uid, req.session._password,
1383 action["report_name"], report_ids,
1384 report_data, context)
1386 report_struct = None
1388 report_struct = report_srv.report_get(
1389 req.session._db, req.session._uid, req.session._password, report_id)
1390 if report_struct["state"]:
1393 time.sleep(self.POLLING_DELAY)
1395 report = base64.b64decode(report_struct['result'])
1396 if report_struct.get('code') == 'zlib':
1397 report = zlib.decompress(report)
1398 report_mimetype = self.TYPES_MAPPING.get(
1399 report_struct['format'], 'octet-stream')
1400 return req.make_response(report,
1402 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1403 ('Content-Type', report_mimetype),
1404 ('Content-Length', len(report))],
1405 cookies={'fileToken': int(token)})
1408 _cp_path = "/web/import"
1410 def fields_get(self, req, model):
1411 Model = req.session.model(model)
1412 fields = Model.fields_get(False, req.session.eval_context(req.context))
1415 @openerpweb.httprequest
1416 def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp):
1418 data = list(csv.reader(
1419 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1420 except csv.Error, e:
1422 return '<script>window.top.%s(%s);</script>' % (
1423 jsonp, simplejson.dumps({'error': {
1424 'message': 'Error parsing CSV file: %s' % e,
1425 # decodes each byte to a unicode character, which may or
1426 # may not be printable, but decoding will succeed.
1427 # Otherwise simplejson will try to decode the `str` using
1428 # utf-8, which is very likely to blow up on characters out
1429 # of the ascii range (in range [128, 256))
1430 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1433 return '<script>window.top.%s(%s);</script>' % (
1434 jsonp, simplejson.dumps(
1435 {'records': data[:10]}, encoding=csvcode))
1436 except UnicodeDecodeError:
1437 return '<script>window.top.%s(%s);</script>' % (
1438 jsonp, simplejson.dumps({
1439 'message': u"Failed to decode CSV file using encoding %s, "
1440 u"try switching to a different encoding" % csvcode
1443 @openerpweb.httprequest
1444 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1446 modle_obj = req.session.model(model)
1447 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1448 simplejson.loads(meta))
1451 if not (csvdel and len(csvdel) == 1):
1452 error = u"The CSV delimiter must be a single character"
1454 if not indices and fields:
1455 error = u"You must select at least one field to import"
1458 return '<script>window.top.%s(%s);</script>' % (
1459 jsonp, simplejson.dumps({'error': {'message': error}}))
1461 # skip ignored records
1462 data_record = itertools.islice(
1463 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1466 # if only one index, itemgetter will return an atom rather than a tuple
1467 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1468 else: mapper = operator.itemgetter(*indices)
1473 # decode each data row
1475 [record.decode(csvcode) for record in row]
1476 for row in itertools.imap(mapper, data_record)
1477 # don't insert completely empty rows (can happen due to fields
1478 # filtering in case of e.g. o2m content rows)
1481 except UnicodeDecodeError:
1482 error = u"Failed to decode CSV file using encoding %s" % csvcode
1483 except csv.Error, e:
1484 error = u"Could not process CSV file: %s" % e
1486 # If the file contains nothing,
1488 error = u"File to import is empty"
1490 return '<script>window.top.%s(%s);</script>' % (
1491 jsonp, simplejson.dumps({'error': {'message': error}}))
1494 (code, record, message, _nope) = modle_obj.import_data(
1495 fields, data, 'init', '', False,
1496 req.session.eval_context(req.context))
1497 except xmlrpclib.Fault, e:
1498 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1499 return '<script>window.top.%s(%s);</script>' % (
1500 jsonp, simplejson.dumps({'error':error}))
1503 return '<script>window.top.%s(%s);</script>' % (
1504 jsonp, simplejson.dumps({'success':True}))
1506 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1508 return '<script>window.top.%s(%s);</script>' % (
1509 jsonp, simplejson.dumps({'error': {'message':msg}}))