1 # -*- coding: utf-8 -*-
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
19 import babel.messages.pofile
22 openerpweb = web.common.http
24 #----------------------------------------------------------
25 # OpenERP Web web Controllers
26 #----------------------------------------------------------
28 # TODO change into concat_file(addons,key) taking care of addons_path
29 def concat_files(addons_path, file_list):
30 """ Concatenate file content
31 return (concat,timestamp)
32 concat: concatenation of file content
33 timestamp: max(os.path.getmtime of file_list)
38 fname = os.path.join(addons_path, i[1:])
39 ftime = os.path.getmtime(fname)
40 if ftime > files_timestamp:
41 files_timestamp = ftime
42 files_content.append(open(fname).read())
43 files_concat = "".join(files_content)
44 return files_concat,files_timestamp
46 home_template = textwrap.dedent("""<!DOCTYPE html>
47 <html style="height: 100%%">
49 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
50 <title>OpenERP</title>
51 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
54 <script type="text/javascript">
56 var c = new openerp.init(%(modules)s);
57 var wc = new c.web.WebClient("oe");
62 <body id="oe" class="openerp"></body>
65 class WebClient(openerpweb.Controller):
66 _cp_path = "/web/webclient"
68 def server_wide_modules(self, req):
69 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
72 def manifest_glob(self, req, addons, key):
74 addons = self.server_wide_modules(req)
76 addons = addons.split(',')
79 manifest = openerpweb.addons_manifest.get(addon, None)
82 addons_path = manifest['addons_path']
83 globlist = manifest.get(key, [])
84 for pattern in globlist:
85 for path in glob.glob(os.path.join(addons_path, addon, pattern)):
86 files.append(path[len(addons_path):])
89 @openerpweb.jsonrequest
90 def csslist(self, req, mods=None):
91 return self.manifest_glob(req, mods, 'css')
93 @openerpweb.jsonrequest
94 def jslist(self, req, mods=None):
95 return self.manifest_glob(req, mods, 'js')
97 @openerpweb.httprequest
98 def css(self, req, mods=None):
99 files = self.manifest_glob(req, mods, 'css')
100 content,timestamp = concat_files(req.config.addons_path, files)
101 # TODO request set the Date of last modif and Etag
102 return req.make_response(content, [('Content-Type', 'text/css')])
104 @openerpweb.httprequest
105 def js(self, req, mods=None):
106 files = self.manifest_glob(req, mods, 'js')
107 content,timestamp = concat_files(req.config.addons_path, files)
108 # TODO request set the Date of last modif and Etag
109 return req.make_response(content, [('Content-Type', 'application/javascript')])
111 @openerpweb.httprequest
112 def home(self, req, s_action=None, **kw):
114 jslist = ['/web/webclient/js']
116 jslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'js')]
117 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
120 csslist = ['/web/webclient/css']
122 csslist = [i + '?debug=' + str(time.time()) for i in self.manifest_glob(req, None, 'css')]
123 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
125 r = home_template % {
128 'modules': simplejson.dumps(self.server_wide_modules(req)),
132 @openerpweb.jsonrequest
133 def translations(self, req, mods, lang):
134 lang_model = req.session.model('res.lang')
135 ids = lang_model.search([("code", "=", lang)])
137 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
138 "grouping", "decimal_point", "thousands_sep"])
142 if lang.count("_") > 0:
146 langs = lang.split(separator)
147 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
150 for addon_name in mods:
151 transl = {"messages":[]}
152 transs[addon_name] = transl
154 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
155 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
156 if not os.path.exists(f_name):
159 with open(f_name) as t_file:
160 po = babel.messages.pofile.read_po(t_file)
164 if x.id and x.string:
165 transl["messages"].append({'id': x.id, 'string': x.string})
166 return {"modules": transs,
167 "lang_parameters": lang_obj}
169 @openerpweb.jsonrequest
170 def version_info(self, req):
172 "version": web.common.release.version
175 class Database(openerpweb.Controller):
176 _cp_path = "/web/database"
178 @openerpweb.jsonrequest
179 def get_list(self, req):
180 proxy = req.session.proxy("db")
182 h = req.httprequest.headers['Host'].split(':')[0]
184 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
185 dbs = [i for i in dbs if re.match(r, i)]
186 return {"db_list": dbs}
188 @openerpweb.jsonrequest
189 def progress(self, req, password, id):
190 return req.session.proxy('db').get_progress(password, id)
192 @openerpweb.jsonrequest
193 def create(self, req, fields):
195 params = dict(map(operator.itemgetter('name', 'value'), fields))
197 params['super_admin_pwd'],
199 bool(params.get('demo_data')),
201 params['create_admin_pwd']
205 return req.session.proxy("db").create(*create_attrs)
206 except xmlrpclib.Fault, e:
207 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
208 return {'error': e.faultCode, 'title': 'Create Database'}
209 return {'error': 'Could not create database !', 'title': 'Create Database'}
211 @openerpweb.jsonrequest
212 def drop(self, req, fields):
213 password, db = operator.itemgetter(
214 'drop_pwd', 'drop_db')(
215 dict(map(operator.itemgetter('name', 'value'), fields)))
218 return req.session.proxy("db").drop(password, db)
219 except xmlrpclib.Fault, e:
220 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
221 return {'error': e.faultCode, 'title': 'Drop Database'}
222 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
224 @openerpweb.httprequest
225 def backup(self, req, backup_db, backup_pwd, token):
227 db_dump = base64.b64decode(
228 req.session.proxy("db").dump(backup_pwd, backup_db))
229 return req.make_response(db_dump,
230 [('Content-Type', 'application/octet-stream; charset=binary'),
231 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
232 {'fileToken': int(token)}
234 except xmlrpclib.Fault, e:
235 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
236 return 'Backup Database|' + e.faultCode
237 return 'Backup Database|Could not generate database backup'
239 @openerpweb.httprequest
240 def restore(self, req, db_file, restore_pwd, new_db):
242 data = base64.b64encode(db_file.file.read())
243 req.session.proxy("db").restore(restore_pwd, new_db, data)
245 except xmlrpclib.Fault, e:
246 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
247 raise Exception("AccessDenied")
249 @openerpweb.jsonrequest
250 def change_password(self, req, fields):
251 old_password, new_password = operator.itemgetter(
252 'old_pwd', 'new_pwd')(
253 dict(map(operator.itemgetter('name', 'value'), fields)))
255 return req.session.proxy("db").change_admin_password(old_password, new_password)
256 except xmlrpclib.Fault, e:
257 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
258 return {'error': e.faultCode, 'title': 'Change Password'}
259 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
261 class Session(openerpweb.Controller):
262 _cp_path = "/web/session"
264 @openerpweb.jsonrequest
265 def login(self, req, db, login, password):
266 req.session.login(db, login, password)
267 ctx = req.session.get_context() if req.session._uid else {}
270 "session_id": req.session_id,
271 "uid": req.session._uid,
273 "db": req.session._db
276 @openerpweb.jsonrequest
277 def get_session_info(self, req):
278 req.session.assert_valid(force=True)
280 "uid": req.session._uid,
281 "context": req.session.get_context() if req.session._uid else False,
282 "db": req.session._db
285 @openerpweb.jsonrequest
286 def change_password (self,req,fields):
287 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
288 dict(map(operator.itemgetter('name', 'value'), fields)))
289 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
290 return {'error':'All passwords have to be filled.','title': 'Change Password'}
291 if new_password != confirm_password:
292 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
294 if req.session.model('res.users').change_password(
295 old_password, new_password):
296 return {'new_password':new_password}
298 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
299 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
301 @openerpweb.jsonrequest
302 def sc_list(self, req):
303 return req.session.model('ir.ui.view_sc').get_sc(
304 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
306 @openerpweb.jsonrequest
307 def get_lang_list(self, req):
310 'lang_list': (req.session.proxy("db").list_lang() or []),
314 return {"error": e, "title": "Languages"}
316 @openerpweb.jsonrequest
317 def modules(self, req):
318 # Compute available candidates module
319 loadable = openerpweb.addons_manifest.keys()
320 loaded = req.config.server_wide_modules
321 candidates = [mod for mod in loadable if mod not in loaded]
323 # Compute active true modules that might be on the web side only
324 active = [(name,1) for name in candidates if openerpweb.addons_manifest[name].get('active')]
327 # Retrieve database installed modules
328 module_obj = req.session.model('ir.module.module')
329 installed = [(i['name'],1) for i in module_obj.search_read([('state','=','installed'), ('name','in', candidates)])]
333 merged = dict(installed)
334 merged.update(active)
339 @openerpweb.jsonrequest
340 def eval_domain_and_context(self, req, contexts, domains,
342 """ Evaluates sequences of domains and contexts, composing them into
343 a single context, domain or group_by sequence.
345 :param list contexts: list of contexts to merge together. Contexts are
346 evaluated in sequence, all previous contexts
347 are part of their own evaluation context
348 (starting at the session context).
349 :param list domains: list of domains to merge together. Domains are
350 evaluated in sequence and appended to one another
351 (implicit AND), their evaluation domain is the
352 result of merging all contexts.
353 :param list group_by_seq: list of domains (which may be in a different
354 order than the ``contexts`` parameter),
355 evaluated in sequence, their ``'group_by'``
356 key is extracted if they have one.
361 the global context created by merging all of
365 the concatenation of all domains
368 a list of fields to group by, potentially empty (in which case
369 no group by should be performed)
371 context, domain = eval_context_and_domain(req.session,
372 web.common.nonliterals.CompoundContext(*(contexts or [])),
373 web.common.nonliterals.CompoundDomain(*(domains or [])))
375 group_by_sequence = []
376 for candidate in (group_by_seq or []):
377 ctx = req.session.eval_context(candidate, context)
378 group_by = ctx.get('group_by')
381 elif isinstance(group_by, basestring):
382 group_by_sequence.append(group_by)
384 group_by_sequence.extend(group_by)
389 'group_by': group_by_sequence
392 @openerpweb.jsonrequest
393 def save_session_action(self, req, the_action):
395 This method store an action object in the session object and returns an integer
396 identifying that action. The method get_session_action() can be used to get
399 :param the_action: The action to save in the session.
400 :type the_action: anything
401 :return: A key identifying the saved action.
404 saved_actions = req.httpsession.get('saved_actions')
405 if not saved_actions:
406 saved_actions = {"next":0, "actions":{}}
407 req.httpsession['saved_actions'] = saved_actions
408 # we don't allow more than 10 stored actions
409 if len(saved_actions["actions"]) >= 10:
410 del saved_actions["actions"][min(saved_actions["actions"].keys())]
411 key = saved_actions["next"]
412 saved_actions["actions"][key] = the_action
413 saved_actions["next"] = key + 1
416 @openerpweb.jsonrequest
417 def get_session_action(self, req, key):
419 Gets back a previously saved action. This method can return None if the action
420 was saved since too much time (this case should be handled in a smart way).
422 :param key: The key given by save_session_action()
424 :return: The saved action or None.
427 saved_actions = req.httpsession.get('saved_actions')
428 if not saved_actions:
430 return saved_actions["actions"].get(key)
432 @openerpweb.jsonrequest
433 def check(self, req):
434 req.session.assert_valid()
437 def eval_context_and_domain(session, context, domain=None):
438 e_context = session.eval_context(context)
439 # should we give the evaluated context as an evaluation context to the domain?
440 e_domain = session.eval_domain(domain or [])
442 return e_context, e_domain
444 def load_actions_from_ir_values(req, key, key2, models, meta):
445 context = req.session.eval_context(req.context)
446 Values = req.session.model('ir.values')
447 actions = Values.get(key, key2, models, meta, context)
449 return [(id, name, clean_action(req, action))
450 for id, name, action in actions]
452 def clean_action(req, action):
453 action.setdefault('flags', {})
455 context = req.session.eval_context(req.context)
456 eval_ctx = req.session.evaluation_context(context)
458 # values come from the server, we can just eval them
459 if isinstance(action.get('context'), basestring):
460 action['context'] = eval( action['context'], eval_ctx ) or {}
462 if isinstance(action.get('domain'), basestring):
463 action['domain'] = eval( action['domain'], eval_ctx ) or []
465 if 'type' not in action:
466 action['type'] = 'ir.actions.act_window_close'
468 if action['type'] == 'ir.actions.act_window':
469 return fix_view_modes(action)
472 # I think generate_views,fix_view_modes should go into js ActionManager
473 def generate_views(action):
475 While the server generates a sequence called "views" computing dependencies
476 between a bunch of stuff for views coming directly from the database
477 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
478 to return custom view dictionaries generated on the fly.
480 In that case, there is no ``views`` key available on the action.
482 Since the web client relies on ``action['views']``, generate it here from
483 ``view_mode`` and ``view_id``.
485 Currently handles two different cases:
487 * no view_id, multiple view_mode
488 * single view_id, single view_mode
490 :param dict action: action descriptor dictionary to generate a views key for
492 view_id = action.get('view_id', False)
493 if isinstance(view_id, (list, tuple)):
496 # providing at least one view mode is a requirement, not an option
497 view_modes = action['view_mode'].split(',')
499 if len(view_modes) > 1:
501 raise ValueError('Non-db action dictionaries should provide '
502 'either multiple view modes or a single view '
503 'mode and an optional view id.\n\n Got view '
504 'modes %r and view id %r for action %r' % (
505 view_modes, view_id, action))
506 action['views'] = [(False, mode) for mode in view_modes]
508 action['views'] = [(view_id, view_modes[0])]
510 def fix_view_modes(action):
511 """ For historical reasons, OpenERP has weird dealings in relation to
512 view_mode and the view_type attribute (on window actions):
514 * one of the view modes is ``tree``, which stands for both list views
516 * the choice is made by checking ``view_type``, which is either
517 ``form`` for a list view or ``tree`` for an actual tree view
519 This methods simply folds the view_type into view_mode by adding a
520 new view mode ``list`` which is the result of the ``tree`` view_mode
521 in conjunction with the ``form`` view_type.
523 TODO: this should go into the doc, some kind of "peculiarities" section
525 :param dict action: an action descriptor
526 :returns: nothing, the action is modified in place
528 if 'views' not in action:
529 generate_views(action)
531 if action.pop('view_type') != 'form':
535 [id, mode if mode != 'tree' else 'list']
536 for id, mode in action['views']
541 class Menu(openerpweb.Controller):
542 _cp_path = "/web/menu"
544 @openerpweb.jsonrequest
546 return {'data': self.do_load(req)}
548 def do_load(self, req):
549 """ Loads all menu items (all applications and their sub-menus).
551 :param req: A request object, with an OpenERP session attribute
552 :type req: < session -> OpenERPSession >
553 :return: the menu root
554 :rtype: dict('children': menu_nodes)
556 Menus = req.session.model('ir.ui.menu')
557 # menus are loaded fully unlike a regular tree view, cause there are
558 # less than 512 items
559 context = req.session.eval_context(req.context)
560 menu_ids = Menus.search([], 0, False, False, context)
561 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
562 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
563 menu_items.append(menu_root)
565 # make a tree using parent_id
566 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
567 for menu_item in menu_items:
568 if menu_item['parent_id']:
569 parent = menu_item['parent_id'][0]
572 if parent in menu_items_map:
573 menu_items_map[parent].setdefault(
574 'children', []).append(menu_item)
576 # sort by sequence a tree using parent_id
577 for menu_item in menu_items:
578 menu_item.setdefault('children', []).sort(
579 key=lambda x:x["sequence"])
583 @openerpweb.jsonrequest
584 def action(self, req, menu_id):
585 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
586 [('ir.ui.menu', menu_id)], False)
587 return {"action": actions}
589 class DataSet(openerpweb.Controller):
590 _cp_path = "/web/dataset"
592 @openerpweb.jsonrequest
593 def fields(self, req, model):
594 return {'fields': req.session.model(model).fields_get(False,
595 req.session.eval_context(req.context))}
597 @openerpweb.jsonrequest
598 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
599 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
600 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
602 """ Performs a search() followed by a read() (if needed) using the
603 provided search criteria
605 :param req: a JSON-RPC request object
606 :type req: openerpweb.JsonRequest
607 :param str model: the name of the model to search on
608 :param fields: a list of the fields to return in the result records
610 :param int offset: from which index should the results start being returned
611 :param int limit: the maximum number of records to return
612 :param list domain: the search domain for the query
613 :param list sort: sorting directives
614 :returns: A structure (dict) with two keys: ids (all the ids matching
615 the (domain, context) pair) and records (paginated records
616 matching fields selection set)
619 Model = req.session.model(model)
621 context, domain = eval_context_and_domain(
622 req.session, req.context, domain)
624 ids = Model.search(domain, 0, False, sort or False, context)
625 # need to fill the dataset with all ids for the (domain, context) pair,
626 # so search un-paginated and paginate manually before reading
627 paginated_ids = ids[offset:(offset + limit if limit else None)]
628 if fields and fields == ['id']:
629 # shortcut read if we only want the ids
632 'records': map(lambda id: {'id': id}, paginated_ids)
635 records = Model.read(paginated_ids, fields or False, context)
636 records.sort(key=lambda obj: ids.index(obj['id']))
643 @openerpweb.jsonrequest
644 def read(self, req, model, ids, fields=False):
645 return self.do_search_read(req, model, ids, fields)
647 @openerpweb.jsonrequest
648 def get(self, req, model, ids, fields=False):
649 return self.do_get(req, model, ids, fields)
651 def do_get(self, req, model, ids, fields=False):
652 """ Fetches and returns the records of the model ``model`` whose ids
655 The results are in the same order as the inputs, but elements may be
656 missing (if there is no record left for the id)
658 :param req: the JSON-RPC2 request object
659 :type req: openerpweb.JsonRequest
660 :param model: the model to read from
662 :param ids: a list of identifiers
664 :param fields: a list of fields to fetch, ``False`` or empty to fetch
665 all fields in the model
666 :type fields: list | False
667 :returns: a list of records, in the same order as the list of ids
670 Model = req.session.model(model)
671 records = Model.read(ids, fields, req.session.eval_context(req.context))
673 record_map = dict((record['id'], record) for record in records)
675 return [record_map[id] for id in ids if record_map.get(id)]
677 @openerpweb.jsonrequest
678 def load(self, req, model, id, fields):
679 m = req.session.model(model)
681 r = m.read([id], False, req.session.eval_context(req.context))
684 return {'value': value}
686 @openerpweb.jsonrequest
687 def create(self, req, model, data):
688 m = req.session.model(model)
689 r = m.create(data, req.session.eval_context(req.context))
692 @openerpweb.jsonrequest
693 def save(self, req, model, id, data):
694 m = req.session.model(model)
695 r = m.write([id], data, req.session.eval_context(req.context))
698 @openerpweb.jsonrequest
699 def unlink(self, req, model, ids=()):
700 Model = req.session.model(model)
701 return Model.unlink(ids, req.session.eval_context(req.context))
703 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
704 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
705 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
706 c, d = eval_context_and_domain(req.session, context, domain)
707 if domain_id and len(args) - 1 >= domain_id:
709 if context_id and len(args) - 1 >= context_id:
712 for i in xrange(len(args)):
713 if isinstance(args[i], web.common.nonliterals.BaseContext):
714 args[i] = req.session.eval_context(args[i])
715 if isinstance(args[i], web.common.nonliterals.BaseDomain):
716 args[i] = req.session.eval_domain(args[i])
718 return getattr(req.session.model(model), method)(*args)
720 @openerpweb.jsonrequest
721 def call(self, req, model, method, args, domain_id=None, context_id=None):
722 return self.call_common(req, model, method, args, domain_id, context_id)
724 @openerpweb.jsonrequest
725 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
726 action = self.call_common(req, model, method, args, domain_id, context_id)
727 if isinstance(action, dict) and action.get('type') != '':
728 return {'result': clean_action(req, action)}
729 return {'result': False}
731 @openerpweb.jsonrequest
732 def exec_workflow(self, req, model, id, signal):
733 r = req.session.exec_workflow(model, id, signal)
736 @openerpweb.jsonrequest
737 def default_get(self, req, model, fields):
738 Model = req.session.model(model)
739 return Model.default_get(fields, req.session.eval_context(req.context))
741 @openerpweb.jsonrequest
742 def name_search(self, req, model, search_str, domain=[], context={}):
743 m = req.session.model(model)
744 r = m.name_search(search_str+'%', domain, '=ilike', context)
747 class DataGroup(openerpweb.Controller):
748 _cp_path = "/web/group"
749 @openerpweb.jsonrequest
750 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
751 Model = req.session.model(model)
752 context, domain = eval_context_and_domain(req.session, req.context, domain)
754 return Model.read_group(
755 domain or [], fields, group_by_fields, 0, False,
756 dict(context, group_by=group_by_fields), sort or False)
758 class View(openerpweb.Controller):
759 _cp_path = "/web/view"
761 def fields_view_get(self, req, model, view_id, view_type,
762 transform=True, toolbar=False, submenu=False):
763 Model = req.session.model(model)
764 context = req.session.eval_context(req.context)
765 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
766 # todo fme?: check that we should pass the evaluated context here
767 self.process_view(req.session, fvg, context, transform)
768 if toolbar and transform:
769 self.process_toolbar(req, fvg['toolbar'])
772 def process_view(self, session, fvg, context, transform):
773 # depending on how it feels, xmlrpclib.ServerProxy can translate
774 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
775 # enjoy unicode strings which can not be trivially converted to
776 # strings, and it blows up during parsing.
778 # So ensure we fix this retardation by converting view xml back to
780 if isinstance(fvg['arch'], unicode):
781 arch = fvg['arch'].encode('utf-8')
786 evaluation_context = session.evaluation_context(context or {})
787 xml = self.transform_view(arch, session, evaluation_context)
789 xml = ElementTree.fromstring(arch)
790 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
792 for field in fvg['fields'].itervalues():
793 if field.get('views'):
794 for view in field["views"].itervalues():
795 self.process_view(session, view, None, transform)
796 if field.get('domain'):
797 field["domain"] = self.parse_domain(field["domain"], session)
798 if field.get('context'):
799 field["context"] = self.parse_context(field["context"], session)
801 def process_toolbar(self, req, toolbar):
803 The toolbar is a mapping of section_key: [action_descriptor]
805 We need to clean all those actions in order to ensure correct
808 for actions in toolbar.itervalues():
809 for action in actions:
810 if 'context' in action:
811 action['context'] = self.parse_context(
812 action['context'], req.session)
813 if 'domain' in action:
814 action['domain'] = self.parse_domain(
815 action['domain'], req.session)
817 @openerpweb.jsonrequest
818 def add_custom(self, req, view_id, arch):
819 CustomView = req.session.model('ir.ui.view.custom')
821 'user_id': req.session._uid,
824 }, req.session.eval_context(req.context))
825 return {'result': True}
827 @openerpweb.jsonrequest
828 def undo_custom(self, req, view_id, reset=False):
829 CustomView = req.session.model('ir.ui.view.custom')
830 context = req.session.eval_context(req.context)
831 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
832 0, False, False, context)
835 CustomView.unlink(vcustom, context)
837 CustomView.unlink([vcustom[0]], context)
838 return {'result': True}
839 return {'result': False}
841 def transform_view(self, view_string, session, context=None):
842 # transform nodes on the fly via iterparse, instead of
843 # doing it statically on the parsing result
844 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
846 for event, elem in parser:
850 self.parse_domains_and_contexts(elem, session)
853 def parse_domain(self, domain, session):
854 """ Parses an arbitrary string containing a domain, transforms it
855 to either a literal domain or a :class:`web.common.nonliterals.Domain`
857 :param domain: the domain to parse, if the domain is not a string it
858 is assumed to be a literal domain and is returned as-is
859 :param session: Current OpenERP session
860 :type session: openerpweb.openerpweb.OpenERPSession
862 if not isinstance(domain, (str, unicode)):
865 return ast.literal_eval(domain)
868 return web.common.nonliterals.Domain(session, domain)
870 def parse_context(self, context, session):
871 """ Parses an arbitrary string containing a context, transforms it
872 to either a literal context or a :class:`web.common.nonliterals.Context`
874 :param context: the context to parse, if the context is not a string it
875 is assumed to be a literal domain and is returned as-is
876 :param session: Current OpenERP session
877 :type session: openerpweb.openerpweb.OpenERPSession
879 if not isinstance(context, (str, unicode)):
882 return ast.literal_eval(context)
884 return web.common.nonliterals.Context(session, context)
886 def parse_domains_and_contexts(self, elem, session):
887 """ Converts domains and contexts from the view into Python objects,
888 either literals if they can be parsed by literal_eval or a special
889 placeholder object if the domain or context refers to free variables.
891 :param elem: the current node being parsed
892 :type param: xml.etree.ElementTree.Element
893 :param session: OpenERP session object, used to store and retrieve
895 :type session: openerpweb.openerpweb.OpenERPSession
897 for el in ['domain', 'filter_domain']:
898 domain = elem.get(el, '').strip()
900 elem.set(el, self.parse_domain(domain, session))
901 for el in ['context', 'default_get']:
902 context_string = elem.get(el, '').strip()
904 elem.set(el, self.parse_context(context_string, session))
906 @openerpweb.jsonrequest
907 def load(self, req, model, view_id, view_type, toolbar=False):
908 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
910 class ListView(View):
911 _cp_path = "/web/listview"
913 def process_colors(self, view, row, context):
914 colors = view['arch']['attrs'].get('colors')
921 for pair in colors.split(';')
922 if eval(pair.split(':')[1], dict(context, **row))
927 elif len(color) == 1:
931 class TreeView(View):
932 _cp_path = "/web/treeview"
934 @openerpweb.jsonrequest
935 def action(self, req, model, id):
936 return load_actions_from_ir_values(
937 req,'action', 'tree_but_open',[(model, id)],
940 class SearchView(View):
941 _cp_path = "/web/searchview"
943 @openerpweb.jsonrequest
944 def load(self, req, model, view_id):
945 fields_view = self.fields_view_get(req, model, view_id, 'search')
946 return {'fields_view': fields_view}
948 @openerpweb.jsonrequest
949 def fields_get(self, req, model):
950 Model = req.session.model(model)
951 fields = Model.fields_get(False, req.session.eval_context(req.context))
952 for field in fields.values():
953 # shouldn't convert the views too?
954 if field.get('domain'):
955 field["domain"] = self.parse_domain(field["domain"], req.session)
956 if field.get('context'):
957 field["context"] = self.parse_domain(field["context"], req.session)
958 return {'fields': fields}
960 @openerpweb.jsonrequest
961 def get_filters(self, req, model):
962 Model = req.session.model("ir.filters")
963 filters = Model.get_filters(model)
964 for filter in filters:
965 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
966 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
969 @openerpweb.jsonrequest
970 def save_filter(self, req, model, name, context_to_save, domain):
971 Model = req.session.model("ir.filters")
972 ctx = web.common.nonliterals.CompoundContext(context_to_save)
973 ctx.session = req.session
975 domain = web.common.nonliterals.CompoundDomain(domain)
976 domain.session = req.session
977 domain = domain.evaluate()
978 uid = req.session._uid
979 context = req.session.eval_context(req.context)
980 to_return = Model.create_or_replace({"context": ctx,
988 class Binary(openerpweb.Controller):
989 _cp_path = "/web/binary"
991 @openerpweb.httprequest
992 def image(self, req, model, id, field, **kw):
993 Model = req.session.model(model)
994 context = req.session.eval_context(req.context)
998 res = Model.default_get([field], context).get(field)
1000 res = Model.read([int(id)], [field], context)[0].get(field)
1001 image_data = base64.b64decode(res)
1002 except (TypeError, xmlrpclib.Fault):
1003 image_data = self.placeholder(req)
1004 return req.make_response(image_data, [
1005 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1006 def placeholder(self, req):
1007 addons_path = openerpweb.addons_manifest['web']['addons_path']
1008 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1010 @openerpweb.httprequest
1011 def saveas(self, req, model, id, field, fieldname, **kw):
1012 Model = req.session.model(model)
1013 context = req.session.eval_context(req.context)
1014 res = Model.read([int(id)], [field, fieldname], context)[0]
1015 filecontent = res.get(field, '')
1017 return req.not_found()
1019 filename = '%s_%s' % (model.replace('.', '_'), id)
1021 filename = res.get(fieldname, '') or filename
1022 return req.make_response(filecontent,
1023 [('Content-Type', 'application/octet-stream'),
1024 ('Content-Disposition', 'attachment; filename=' + filename)])
1026 @openerpweb.httprequest
1027 def upload(self, req, callback, ufile):
1028 # TODO: might be useful to have a configuration flag for max-length file uploads
1030 out = """<script language="javascript" type="text/javascript">
1031 var win = window.top.window,
1033 if (typeof(callback) === 'function') {
1034 callback.apply(this, %s);
1036 win.jQuery('#oe_notification', win.document).notify('create', {
1037 title: "Ajax File Upload",
1038 text: "Could not find callback"
1043 args = [ufile.content_length, ufile.filename,
1044 ufile.content_type, base64.b64encode(data)]
1045 except Exception, e:
1046 args = [False, e.message]
1047 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1049 @openerpweb.httprequest
1050 def upload_attachment(self, req, callback, model, id, ufile):
1051 context = req.session.eval_context(req.context)
1052 Model = req.session.model('ir.attachment')
1054 out = """<script language="javascript" type="text/javascript">
1055 var win = window.top.window,
1057 if (typeof(callback) === 'function') {
1058 callback.call(this, %s);
1061 attachment_id = Model.create({
1062 'name': ufile.filename,
1063 'datas': base64.encodestring(ufile.read()),
1068 'filename': ufile.filename,
1071 except Exception, e:
1072 args = { 'error': e.message }
1073 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1075 class Action(openerpweb.Controller):
1076 _cp_path = "/web/action"
1078 @openerpweb.jsonrequest
1079 def load(self, req, action_id):
1080 Actions = req.session.model('ir.actions.actions')
1082 context = req.session.eval_context(req.context)
1083 action_type = Actions.read([action_id], ['type'], context)
1086 if action_type[0]['type'] == 'ir.actions.report.xml':
1087 ctx.update({'bin_size': True})
1089 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1091 value = clean_action(req, action[0])
1092 return {'result': value}
1094 @openerpweb.jsonrequest
1095 def run(self, req, action_id):
1096 return clean_action(req, req.session.model('ir.actions.server').run(
1097 [action_id], req.session.eval_context(req.context)))
1100 _cp_path = "/web/export"
1102 @openerpweb.jsonrequest
1103 def formats(self, req):
1104 """ Returns all valid export formats
1106 :returns: for each export format, a pair of identifier and printable name
1107 :rtype: [(str, str)]
1111 for path, controller in openerpweb.controllers_path.iteritems()
1112 if path.startswith(self._cp_path)
1113 if hasattr(controller, 'fmt')
1114 ], key=operator.itemgetter(1))
1116 def fields_get(self, req, model):
1117 Model = req.session.model(model)
1118 fields = Model.fields_get(False, req.session.eval_context(req.context))
1121 @openerpweb.jsonrequest
1122 def get_fields(self, req, model, prefix='', parent_name= '',
1123 import_compat=True, parent_field_type=None):
1125 if import_compat and parent_field_type == "many2one":
1128 fields = self.fields_get(req, model)
1129 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1131 fields_sequence = sorted(fields.iteritems(),
1132 key=lambda field: field[1].get('string', ''))
1135 for field_name, field in fields_sequence:
1136 if import_compat and field.get('readonly'):
1137 # If none of the field's states unsets readonly, skip the field
1138 if all(dict(attrs).get('readonly', True)
1139 for attrs in field.get('states', {}).values()):
1142 id = prefix + (prefix and '/'or '') + field_name
1143 name = parent_name + (parent_name and '/' or '') + field['string']
1144 record = {'id': id, 'string': name,
1145 'value': id, 'children': False,
1146 'field_type': field.get('type'),
1147 'required': field.get('required')}
1148 records.append(record)
1150 if len(name.split('/')) < 3 and 'relation' in field:
1151 ref = field.pop('relation')
1152 record['value'] += '/id'
1153 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1155 if not import_compat or field['type'] == 'one2many':
1156 # m2m field in import_compat is childless
1157 record['children'] = True
1161 @openerpweb.jsonrequest
1162 def namelist(self,req, model, export_id):
1163 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1164 export = req.session.model("ir.exports").read([export_id])[0]
1165 export_fields_list = req.session.model("ir.exports.line").read(
1166 export['export_fields'])
1168 fields_data = self.fields_info(
1169 req, model, map(operator.itemgetter('name'), export_fields_list))
1172 {'name': field['name'], 'label': fields_data[field['name']]}
1173 for field in export_fields_list
1176 def fields_info(self, req, model, export_fields):
1178 fields = self.fields_get(req, model)
1179 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1181 # To make fields retrieval more efficient, fetch all sub-fields of a
1182 # given field at the same time. Because the order in the export list is
1183 # arbitrary, this requires ordering all sub-fields of a given field
1184 # together so they can be fetched at the same time
1186 # Works the following way:
1187 # * sort the list of fields to export, the default sorting order will
1188 # put the field itself (if present, for xmlid) and all of its
1189 # sub-fields right after it
1190 # * then, group on: the first field of the path (which is the same for
1191 # a field and for its subfields and the length of splitting on the
1192 # first '/', which basically means grouping the field on one side and
1193 # all of the subfields on the other. This way, we have the field (for
1194 # the xmlid) with length 1, and all of the subfields with the same
1195 # base but a length "flag" of 2
1196 # * if we have a normal field (length 1), just add it to the info
1197 # mapping (with its string) as-is
1198 # * otherwise, recursively call fields_info via graft_subfields.
1199 # all graft_subfields does is take the result of fields_info (on the
1200 # field's model) and prepend the current base (current field), which
1201 # rebuilds the whole sub-tree for the field
1203 # result: because we're not fetching the fields_get for half the
1204 # database models, fetching a namelist with a dozen fields (including
1205 # relational data) falls from ~6s to ~300ms (on the leads model).
1206 # export lists with no sub-fields (e.g. import_compatible lists with
1207 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1208 # there's a single fields_get to execute)
1209 for (base, length), subfields in itertools.groupby(
1210 sorted(export_fields),
1211 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1212 subfields = list(subfields)
1214 # subfields is a seq of $base/*rest, and not loaded yet
1215 info.update(self.graft_subfields(
1216 req, fields[base]['relation'], base, fields[base]['string'],
1220 info[base] = fields[base]['string']
1224 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1225 export_fields = [field.split('/', 1)[1] for field in fields]
1227 (prefix + '/' + k, prefix_string + '/' + v)
1228 for k, v in self.fields_info(req, model, export_fields).iteritems())
1230 #noinspection PyPropertyDefinition
1232 def content_type(self):
1233 """ Provides the format's content type """
1234 raise NotImplementedError()
1236 def filename(self, base):
1237 """ Creates a valid filename for the format (with extension) from the
1238 provided base name (exension-less)
1240 raise NotImplementedError()
1242 def from_data(self, fields, rows):
1243 """ Conversion method from OpenERP's export data to whatever the
1244 current export class outputs
1246 :params list fields: a list of fields to export
1247 :params list rows: a list of records to export
1251 raise NotImplementedError()
1253 @openerpweb.httprequest
1254 def index(self, req, data, token):
1255 model, fields, ids, domain, import_compat = \
1256 operator.itemgetter('model', 'fields', 'ids', 'domain',
1258 simplejson.loads(data))
1260 context = req.session.eval_context(req.context)
1261 Model = req.session.model(model)
1262 ids = ids or Model.search(domain, context=context)
1264 field_names = map(operator.itemgetter('name'), fields)
1265 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1268 columns_headers = field_names
1270 columns_headers = [val['label'].strip() for val in fields]
1273 return req.make_response(self.from_data(columns_headers, import_data),
1274 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1275 ('Content-Type', self.content_type)],
1276 cookies={'fileToken': int(token)})
1278 class CSVExport(Export):
1279 _cp_path = '/web/export/csv'
1280 fmt = ('csv', 'CSV')
1283 def content_type(self):
1284 return 'text/csv;charset=utf8'
1286 def filename(self, base):
1287 return base + '.csv'
1289 def from_data(self, fields, rows):
1291 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1293 writer.writerow(fields)
1298 if isinstance(d, basestring):
1299 d = d.replace('\n',' ').replace('\t',' ')
1301 d = d.encode('utf-8')
1304 if d is False: d = None
1306 writer.writerow(row)
1313 class ExcelExport(Export):
1314 _cp_path = '/web/export/xls'
1315 fmt = ('xls', 'Excel')
1318 def content_type(self):
1319 return 'application/vnd.ms-excel'
1321 def filename(self, base):
1322 return base + '.xls'
1324 def from_data(self, fields, rows):
1327 workbook = xlwt.Workbook()
1328 worksheet = workbook.add_sheet('Sheet 1')
1330 for i, fieldname in enumerate(fields):
1331 worksheet.write(0, i, str(fieldname))
1332 worksheet.col(i).width = 8000 # around 220 pixels
1334 style = xlwt.easyxf('align: wrap yes')
1336 for row_index, row in enumerate(rows):
1337 for cell_index, cell_value in enumerate(row):
1338 if isinstance(cell_value, basestring):
1339 cell_value = re.sub("\r", " ", cell_value)
1340 worksheet.write(row_index + 1, cell_index, cell_value, style)
1349 class Reports(View):
1350 _cp_path = "/web/report"
1351 POLLING_DELAY = 0.25
1353 'doc': 'application/vnd.ms-word',
1354 'html': 'text/html',
1355 'odt': 'application/vnd.oasis.opendocument.text',
1356 'pdf': 'application/pdf',
1357 'sxw': 'application/vnd.sun.xml.writer',
1358 'xls': 'application/vnd.ms-excel',
1361 @openerpweb.httprequest
1362 def index(self, req, action, token):
1363 action = simplejson.loads(action)
1365 report_srv = req.session.proxy("report")
1366 context = req.session.eval_context(
1367 web.common.nonliterals.CompoundContext(
1368 req.context or {}, action[ "context"]))
1371 report_ids = context["active_ids"]
1372 if 'report_type' in action:
1373 report_data['report_type'] = action['report_type']
1374 if 'datas' in action:
1375 if 'form' in action['datas']:
1376 report_data['form'] = action['datas']['form']
1377 if 'ids' in action['datas']:
1378 report_ids = action['datas']['ids']
1380 report_id = report_srv.report(
1381 req.session._db, req.session._uid, req.session._password,
1382 action["report_name"], report_ids,
1383 report_data, context)
1385 report_struct = None
1387 report_struct = report_srv.report_get(
1388 req.session._db, req.session._uid, req.session._password, report_id)
1389 if report_struct["state"]:
1392 time.sleep(self.POLLING_DELAY)
1394 report = base64.b64decode(report_struct['result'])
1395 if report_struct.get('code') == 'zlib':
1396 report = zlib.decompress(report)
1397 report_mimetype = self.TYPES_MAPPING.get(
1398 report_struct['format'], 'octet-stream')
1399 return req.make_response(report,
1401 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1402 ('Content-Type', report_mimetype),
1403 ('Content-Length', len(report))],
1404 cookies={'fileToken': int(token)})
1407 _cp_path = "/web/import"
1409 def fields_get(self, req, model):
1410 Model = req.session.model(model)
1411 fields = Model.fields_get(False, req.session.eval_context(req.context))
1414 @openerpweb.httprequest
1415 def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp):
1417 data = list(csv.reader(
1418 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1419 except csv.Error, e:
1421 return '<script>window.top.%s(%s);</script>' % (
1422 jsonp, simplejson.dumps({'error': {
1423 'message': 'Error parsing CSV file: %s' % e,
1424 # decodes each byte to a unicode character, which may or
1425 # may not be printable, but decoding will succeed.
1426 # Otherwise simplejson will try to decode the `str` using
1427 # utf-8, which is very likely to blow up on characters out
1428 # of the ascii range (in range [128, 256))
1429 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1432 return '<script>window.top.%s(%s);</script>' % (
1433 jsonp, simplejson.dumps(
1434 {'records': data[:10]}, encoding=csvcode))
1435 except UnicodeDecodeError:
1436 return '<script>window.top.%s(%s);</script>' % (
1437 jsonp, simplejson.dumps({
1438 'message': u"Failed to decode CSV file using encoding %s, "
1439 u"try switching to a different encoding" % csvcode
1442 @openerpweb.httprequest
1443 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1445 modle_obj = req.session.model(model)
1446 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1447 simplejson.loads(meta))
1450 if not (csvdel and len(csvdel) == 1):
1451 error = u"The CSV delimiter must be a single character"
1453 if not indices and fields:
1454 error = u"You must select at least one field to import"
1457 return '<script>window.top.%s(%s);</script>' % (
1458 jsonp, simplejson.dumps({'error': {'message': error}}))
1460 # skip ignored records
1461 data_record = itertools.islice(
1462 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1465 # if only one index, itemgetter will return an atom rather than a tuple
1466 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1467 else: mapper = operator.itemgetter(*indices)
1472 # decode each data row
1474 [record.decode(csvcode) for record in row]
1475 for row in itertools.imap(mapper, data_record)
1476 # don't insert completely empty rows (can happen due to fields
1477 # filtering in case of e.g. o2m content rows)
1480 except UnicodeDecodeError:
1481 error = u"Failed to decode CSV file using encoding %s" % csvcode
1482 except csv.Error, e:
1483 error = u"Could not process CSV file: %s" % e
1485 # If the file contains nothing,
1487 error = u"File to import is empty"
1489 return '<script>window.top.%s(%s);</script>' % (
1490 jsonp, simplejson.dumps({'error': {'message': error}}))
1493 (code, record, message, _nope) = modle_obj.import_data(
1494 fields, data, 'init', '', False,
1495 req.session.eval_context(req.context))
1496 except xmlrpclib.Fault, e:
1497 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1498 return '<script>window.top.%s(%s);</script>' % (
1499 jsonp, simplejson.dumps({'error':error}))
1502 return '<script>window.top.%s(%s);</script>' % (
1503 jsonp, simplejson.dumps({'success':True}))
1505 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1507 return '<script>window.top.%s(%s);</script>' % (
1508 jsonp, simplejson.dumps({'error': {'message':msg}}))