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 def concat_files(file_list):
29 """ Concatenate file content
30 return (concat,timestamp)
31 concat: concatenation of file content
32 timestamp: max(os.path.getmtime of file_list)
36 for fname in file_list:
37 ftime = os.path.getmtime(fname)
38 if ftime > files_timestamp:
39 files_timestamp = ftime
40 files_content.append(open(fname).read())
41 files_concat = "".join(files_content)
42 return files_concat,files_timestamp
44 home_template = textwrap.dedent("""<!DOCTYPE html>
45 <html style="height: 100%%">
47 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
48 <title>OpenERP</title>
49 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
52 <script type="text/javascript">
54 var c = new openerp.init(%(modules)s);
55 var wc = new c.web.WebClient("oe");
60 <body id="oe" class="openerp"></body>
63 class WebClient(openerpweb.Controller):
64 _cp_path = "/web/webclient"
66 def server_wide_modules(self, req):
67 addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
70 def manifest_glob(self, req, addons, key):
72 addons = self.server_wide_modules(req)
74 addons = addons.split(',')
76 manifest = openerpweb.addons_manifest.get(addon, None)
79 # ensure does not ends with /
80 addons_path = os.path.join(manifest['addons_path'], '')[:-1]
81 globlist = manifest.get(key, [])
82 for pattern in globlist:
83 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
84 yield path, path[len(addons_path):]
86 def manifest_list(self, req, mods, extension):
88 path = '/web/webclient/' + extension
90 path += '?mods=' + mods
92 return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
94 @openerpweb.jsonrequest
95 def csslist(self, req, mods=None):
96 return self.manifest_list(req, mods, 'css')
98 @openerpweb.jsonrequest
99 def jslist(self, req, mods=None):
100 return self.manifest_list(req, mods, 'js')
102 @openerpweb.httprequest
103 def css(self, req, mods=None):
104 files = [f[0] for f in self.manifest_glob(req, mods, 'css')]
105 content,timestamp = concat_files(files)
106 # TODO use timestamp to set Last mofified date and E-tag
107 return req.make_response(content, [('Content-Type', 'text/css')])
109 @openerpweb.httprequest
110 def js(self, req, mods=None):
111 files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
112 content,timestamp = concat_files(files)
113 # TODO use timestamp to set Last mofified date and E-tag
114 return req.make_response(content, [('Content-Type', 'application/javascript')])
116 @openerpweb.httprequest
117 def home(self, req, s_action=None, **kw):
118 js = "\n ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
119 css = "\n ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
121 r = home_template % {
124 'modules': simplejson.dumps(self.server_wide_modules(req)),
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):
445 action.setdefault('flags', {})
447 context = req.session.eval_context(req.context)
448 eval_ctx = req.session.evaluation_context(context)
450 # values come from the server, we can just eval them
451 if isinstance(action.get('context'), basestring):
452 action['context'] = eval( action['context'], eval_ctx ) or {}
454 if isinstance(action.get('domain'), basestring):
455 action['domain'] = eval( action['domain'], eval_ctx ) or []
457 if 'type' not in action:
458 action['type'] = 'ir.actions.act_window_close'
460 if action['type'] == 'ir.actions.act_window':
461 return fix_view_modes(action)
464 # I think generate_views,fix_view_modes should go into js ActionManager
465 def generate_views(action):
467 While the server generates a sequence called "views" computing dependencies
468 between a bunch of stuff for views coming directly from the database
469 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
470 to return custom view dictionaries generated on the fly.
472 In that case, there is no ``views`` key available on the action.
474 Since the web client relies on ``action['views']``, generate it here from
475 ``view_mode`` and ``view_id``.
477 Currently handles two different cases:
479 * no view_id, multiple view_mode
480 * single view_id, single view_mode
482 :param dict action: action descriptor dictionary to generate a views key for
484 view_id = action.get('view_id', False)
485 if isinstance(view_id, (list, tuple)):
488 # providing at least one view mode is a requirement, not an option
489 view_modes = action['view_mode'].split(',')
491 if len(view_modes) > 1:
493 raise ValueError('Non-db action dictionaries should provide '
494 'either multiple view modes or a single view '
495 'mode and an optional view id.\n\n Got view '
496 'modes %r and view id %r for action %r' % (
497 view_modes, view_id, action))
498 action['views'] = [(False, mode) for mode in view_modes]
500 action['views'] = [(view_id, view_modes[0])]
502 def fix_view_modes(action):
503 """ For historical reasons, OpenERP has weird dealings in relation to
504 view_mode and the view_type attribute (on window actions):
506 * one of the view modes is ``tree``, which stands for both list views
508 * the choice is made by checking ``view_type``, which is either
509 ``form`` for a list view or ``tree`` for an actual tree view
511 This methods simply folds the view_type into view_mode by adding a
512 new view mode ``list`` which is the result of the ``tree`` view_mode
513 in conjunction with the ``form`` view_type.
515 TODO: this should go into the doc, some kind of "peculiarities" section
517 :param dict action: an action descriptor
518 :returns: nothing, the action is modified in place
520 if 'views' not in action:
521 generate_views(action)
523 if action.pop('view_type') != 'form':
527 [id, mode if mode != 'tree' else 'list']
528 for id, mode in action['views']
533 class Menu(openerpweb.Controller):
534 _cp_path = "/web/menu"
536 @openerpweb.jsonrequest
538 return {'data': self.do_load(req)}
540 def do_load(self, req):
541 """ Loads all menu items (all applications and their sub-menus).
543 :param req: A request object, with an OpenERP session attribute
544 :type req: < session -> OpenERPSession >
545 :return: the menu root
546 :rtype: dict('children': menu_nodes)
548 Menus = req.session.model('ir.ui.menu')
549 # menus are loaded fully unlike a regular tree view, cause there are
550 # less than 512 items
551 context = req.session.eval_context(req.context)
552 menu_ids = Menus.search([], 0, False, False, context)
553 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
554 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
555 menu_items.append(menu_root)
557 # make a tree using parent_id
558 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
559 for menu_item in menu_items:
560 if menu_item['parent_id']:
561 parent = menu_item['parent_id'][0]
564 if parent in menu_items_map:
565 menu_items_map[parent].setdefault(
566 'children', []).append(menu_item)
568 # sort by sequence a tree using parent_id
569 for menu_item in menu_items:
570 menu_item.setdefault('children', []).sort(
571 key=lambda x:x["sequence"])
575 @openerpweb.jsonrequest
576 def action(self, req, menu_id):
577 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
578 [('ir.ui.menu', menu_id)], False)
579 return {"action": actions}
581 class DataSet(openerpweb.Controller):
582 _cp_path = "/web/dataset"
584 @openerpweb.jsonrequest
585 def fields(self, req, model):
586 return {'fields': req.session.model(model).fields_get(False,
587 req.session.eval_context(req.context))}
589 @openerpweb.jsonrequest
590 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
591 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
592 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
594 """ Performs a search() followed by a read() (if needed) using the
595 provided search criteria
597 :param req: a JSON-RPC request object
598 :type req: openerpweb.JsonRequest
599 :param str model: the name of the model to search on
600 :param fields: a list of the fields to return in the result records
602 :param int offset: from which index should the results start being returned
603 :param int limit: the maximum number of records to return
604 :param list domain: the search domain for the query
605 :param list sort: sorting directives
606 :returns: A structure (dict) with two keys: ids (all the ids matching
607 the (domain, context) pair) and records (paginated records
608 matching fields selection set)
611 Model = req.session.model(model)
613 context, domain = eval_context_and_domain(
614 req.session, req.context, domain)
616 ids = Model.search(domain, 0, False, sort or False, context)
617 # need to fill the dataset with all ids for the (domain, context) pair,
618 # so search un-paginated and paginate manually before reading
619 paginated_ids = ids[offset:(offset + limit if limit else None)]
620 if fields and fields == ['id']:
621 # shortcut read if we only want the ids
624 'records': map(lambda id: {'id': id}, paginated_ids)
627 records = Model.read(paginated_ids, fields or False, context)
628 records.sort(key=lambda obj: ids.index(obj['id']))
635 @openerpweb.jsonrequest
636 def read(self, req, model, ids, fields=False):
637 return self.do_search_read(req, model, ids, fields)
639 @openerpweb.jsonrequest
640 def get(self, req, model, ids, fields=False):
641 return self.do_get(req, model, ids, fields)
643 def do_get(self, req, model, ids, fields=False):
644 """ Fetches and returns the records of the model ``model`` whose ids
647 The results are in the same order as the inputs, but elements may be
648 missing (if there is no record left for the id)
650 :param req: the JSON-RPC2 request object
651 :type req: openerpweb.JsonRequest
652 :param model: the model to read from
654 :param ids: a list of identifiers
656 :param fields: a list of fields to fetch, ``False`` or empty to fetch
657 all fields in the model
658 :type fields: list | False
659 :returns: a list of records, in the same order as the list of ids
662 Model = req.session.model(model)
663 records = Model.read(ids, fields, req.session.eval_context(req.context))
665 record_map = dict((record['id'], record) for record in records)
667 return [record_map[id] for id in ids if record_map.get(id)]
669 @openerpweb.jsonrequest
670 def load(self, req, model, id, fields):
671 m = req.session.model(model)
673 r = m.read([id], False, req.session.eval_context(req.context))
676 return {'value': value}
678 @openerpweb.jsonrequest
679 def create(self, req, model, data):
680 m = req.session.model(model)
681 r = m.create(data, req.session.eval_context(req.context))
684 @openerpweb.jsonrequest
685 def save(self, req, model, id, data):
686 m = req.session.model(model)
687 r = m.write([id], data, req.session.eval_context(req.context))
690 @openerpweb.jsonrequest
691 def unlink(self, req, model, ids=()):
692 Model = req.session.model(model)
693 return Model.unlink(ids, req.session.eval_context(req.context))
695 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
696 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
697 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
698 c, d = eval_context_and_domain(req.session, context, domain)
699 if domain_id and len(args) - 1 >= domain_id:
701 if context_id and len(args) - 1 >= context_id:
704 for i in xrange(len(args)):
705 if isinstance(args[i], web.common.nonliterals.BaseContext):
706 args[i] = req.session.eval_context(args[i])
707 if isinstance(args[i], web.common.nonliterals.BaseDomain):
708 args[i] = req.session.eval_domain(args[i])
710 return getattr(req.session.model(model), method)(*args)
712 @openerpweb.jsonrequest
713 def call(self, req, model, method, args, domain_id=None, context_id=None):
714 return self.call_common(req, model, method, args, domain_id, context_id)
716 @openerpweb.jsonrequest
717 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
718 action = self.call_common(req, model, method, args, domain_id, context_id)
719 if isinstance(action, dict) and action.get('type') != '':
720 return {'result': clean_action(req, action)}
721 return {'result': False}
723 @openerpweb.jsonrequest
724 def exec_workflow(self, req, model, id, signal):
725 r = req.session.exec_workflow(model, id, signal)
728 @openerpweb.jsonrequest
729 def default_get(self, req, model, fields):
730 Model = req.session.model(model)
731 return Model.default_get(fields, req.session.eval_context(req.context))
733 @openerpweb.jsonrequest
734 def name_search(self, req, model, search_str, domain=[], context={}):
735 m = req.session.model(model)
736 r = m.name_search(search_str+'%', domain, '=ilike', context)
739 class DataGroup(openerpweb.Controller):
740 _cp_path = "/web/group"
741 @openerpweb.jsonrequest
742 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
743 Model = req.session.model(model)
744 context, domain = eval_context_and_domain(req.session, req.context, domain)
746 return Model.read_group(
747 domain or [], fields, group_by_fields, 0, False,
748 dict(context, group_by=group_by_fields), sort or False)
750 class View(openerpweb.Controller):
751 _cp_path = "/web/view"
753 def fields_view_get(self, req, model, view_id, view_type,
754 transform=True, toolbar=False, submenu=False):
755 Model = req.session.model(model)
756 context = req.session.eval_context(req.context)
757 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
758 # todo fme?: check that we should pass the evaluated context here
759 self.process_view(req.session, fvg, context, transform)
760 if toolbar and transform:
761 self.process_toolbar(req, fvg['toolbar'])
764 def process_view(self, session, fvg, context, transform):
765 # depending on how it feels, xmlrpclib.ServerProxy can translate
766 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
767 # enjoy unicode strings which can not be trivially converted to
768 # strings, and it blows up during parsing.
770 # So ensure we fix this retardation by converting view xml back to
772 if isinstance(fvg['arch'], unicode):
773 arch = fvg['arch'].encode('utf-8')
778 evaluation_context = session.evaluation_context(context or {})
779 xml = self.transform_view(arch, session, evaluation_context)
781 xml = ElementTree.fromstring(arch)
782 fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml)
784 for field in fvg['fields'].itervalues():
785 if field.get('views'):
786 for view in field["views"].itervalues():
787 self.process_view(session, view, None, transform)
788 if field.get('domain'):
789 field["domain"] = self.parse_domain(field["domain"], session)
790 if field.get('context'):
791 field["context"] = self.parse_context(field["context"], session)
793 def process_toolbar(self, req, toolbar):
795 The toolbar is a mapping of section_key: [action_descriptor]
797 We need to clean all those actions in order to ensure correct
800 for actions in toolbar.itervalues():
801 for action in actions:
802 if 'context' in action:
803 action['context'] = self.parse_context(
804 action['context'], req.session)
805 if 'domain' in action:
806 action['domain'] = self.parse_domain(
807 action['domain'], req.session)
809 @openerpweb.jsonrequest
810 def add_custom(self, req, view_id, arch):
811 CustomView = req.session.model('ir.ui.view.custom')
813 'user_id': req.session._uid,
816 }, req.session.eval_context(req.context))
817 return {'result': True}
819 @openerpweb.jsonrequest
820 def undo_custom(self, req, view_id, reset=False):
821 CustomView = req.session.model('ir.ui.view.custom')
822 context = req.session.eval_context(req.context)
823 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
824 0, False, False, context)
827 CustomView.unlink(vcustom, context)
829 CustomView.unlink([vcustom[0]], context)
830 return {'result': True}
831 return {'result': False}
833 def transform_view(self, view_string, session, context=None):
834 # transform nodes on the fly via iterparse, instead of
835 # doing it statically on the parsing result
836 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
838 for event, elem in parser:
842 self.parse_domains_and_contexts(elem, session)
845 def parse_domain(self, domain, session):
846 """ Parses an arbitrary string containing a domain, transforms it
847 to either a literal domain or a :class:`web.common.nonliterals.Domain`
849 :param domain: the domain to parse, if the domain is not a string it
850 is assumed to be a literal domain and is returned as-is
851 :param session: Current OpenERP session
852 :type session: openerpweb.openerpweb.OpenERPSession
854 if not isinstance(domain, (str, unicode)):
857 return ast.literal_eval(domain)
860 return web.common.nonliterals.Domain(session, domain)
862 def parse_context(self, context, session):
863 """ Parses an arbitrary string containing a context, transforms it
864 to either a literal context or a :class:`web.common.nonliterals.Context`
866 :param context: the context to parse, if the context is not a string it
867 is assumed to be a literal domain and is returned as-is
868 :param session: Current OpenERP session
869 :type session: openerpweb.openerpweb.OpenERPSession
871 if not isinstance(context, (str, unicode)):
874 return ast.literal_eval(context)
876 return web.common.nonliterals.Context(session, context)
878 def parse_domains_and_contexts(self, elem, session):
879 """ Converts domains and contexts from the view into Python objects,
880 either literals if they can be parsed by literal_eval or a special
881 placeholder object if the domain or context refers to free variables.
883 :param elem: the current node being parsed
884 :type param: xml.etree.ElementTree.Element
885 :param session: OpenERP session object, used to store and retrieve
887 :type session: openerpweb.openerpweb.OpenERPSession
889 for el in ['domain', 'filter_domain']:
890 domain = elem.get(el, '').strip()
892 elem.set(el, self.parse_domain(domain, session))
893 for el in ['context', 'default_get']:
894 context_string = elem.get(el, '').strip()
896 elem.set(el, self.parse_context(context_string, session))
898 @openerpweb.jsonrequest
899 def load(self, req, model, view_id, view_type, toolbar=False):
900 return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
902 class ListView(View):
903 _cp_path = "/web/listview"
905 def process_colors(self, view, row, context):
906 colors = view['arch']['attrs'].get('colors')
913 for pair in colors.split(';')
914 if eval(pair.split(':')[1], dict(context, **row))
919 elif len(color) == 1:
923 class TreeView(View):
924 _cp_path = "/web/treeview"
926 @openerpweb.jsonrequest
927 def action(self, req, model, id):
928 return load_actions_from_ir_values(
929 req,'action', 'tree_but_open',[(model, id)],
932 class SearchView(View):
933 _cp_path = "/web/searchview"
935 @openerpweb.jsonrequest
936 def load(self, req, model, view_id):
937 fields_view = self.fields_view_get(req, model, view_id, 'search')
938 return {'fields_view': fields_view}
940 @openerpweb.jsonrequest
941 def fields_get(self, req, model):
942 Model = req.session.model(model)
943 fields = Model.fields_get(False, req.session.eval_context(req.context))
944 for field in fields.values():
945 # shouldn't convert the views too?
946 if field.get('domain'):
947 field["domain"] = self.parse_domain(field["domain"], req.session)
948 if field.get('context'):
949 field["context"] = self.parse_domain(field["context"], req.session)
950 return {'fields': fields}
952 @openerpweb.jsonrequest
953 def get_filters(self, req, model):
954 Model = req.session.model("ir.filters")
955 filters = Model.get_filters(model)
956 for filter in filters:
957 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
958 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
961 @openerpweb.jsonrequest
962 def save_filter(self, req, model, name, context_to_save, domain):
963 Model = req.session.model("ir.filters")
964 ctx = web.common.nonliterals.CompoundContext(context_to_save)
965 ctx.session = req.session
967 domain = web.common.nonliterals.CompoundDomain(domain)
968 domain.session = req.session
969 domain = domain.evaluate()
970 uid = req.session._uid
971 context = req.session.eval_context(req.context)
972 to_return = Model.create_or_replace({"context": ctx,
980 class Binary(openerpweb.Controller):
981 _cp_path = "/web/binary"
983 @openerpweb.httprequest
984 def image(self, req, model, id, field, **kw):
985 Model = req.session.model(model)
986 context = req.session.eval_context(req.context)
990 res = Model.default_get([field], context).get(field)
992 res = Model.read([int(id)], [field], context)[0].get(field)
993 image_data = base64.b64decode(res)
994 except (TypeError, xmlrpclib.Fault):
995 image_data = self.placeholder(req)
996 return req.make_response(image_data, [
997 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
998 def placeholder(self, req):
999 addons_path = openerpweb.addons_manifest['web']['addons_path']
1000 return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1002 @openerpweb.httprequest
1003 def saveas(self, req, model, id, field, fieldname, **kw):
1004 Model = req.session.model(model)
1005 context = req.session.eval_context(req.context)
1006 res = Model.read([int(id)], [field, fieldname], context)[0]
1007 filecontent = base64.b64decode(res.get(field, ''))
1009 return req.not_found()
1011 filename = '%s_%s' % (model.replace('.', '_'), id)
1013 filename = res.get(fieldname, '') or filename
1014 return req.make_response(filecontent,
1015 [('Content-Type', 'application/octet-stream'),
1016 ('Content-Disposition', 'attachment; filename=' + filename)])
1018 @openerpweb.httprequest
1019 def upload(self, req, callback, ufile):
1020 # TODO: might be useful to have a configuration flag for max-length file uploads
1022 out = """<script language="javascript" type="text/javascript">
1023 var win = window.top.window,
1025 if (typeof(callback) === 'function') {
1026 callback.apply(this, %s);
1028 win.jQuery('#oe_notification', win.document).notify('create', {
1029 title: "Ajax File Upload",
1030 text: "Could not find callback"
1035 args = [ufile.content_length, ufile.filename,
1036 ufile.content_type, base64.b64encode(data)]
1037 except Exception, e:
1038 args = [False, e.message]
1039 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1041 @openerpweb.httprequest
1042 def upload_attachment(self, req, callback, model, id, ufile):
1043 context = req.session.eval_context(req.context)
1044 Model = req.session.model('ir.attachment')
1046 out = """<script language="javascript" type="text/javascript">
1047 var win = window.top.window,
1049 if (typeof(callback) === 'function') {
1050 callback.call(this, %s);
1053 attachment_id = Model.create({
1054 'name': ufile.filename,
1055 'datas': base64.encodestring(ufile.read()),
1060 'filename': ufile.filename,
1063 except Exception, e:
1064 args = { 'error': e.message }
1065 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1067 class Action(openerpweb.Controller):
1068 _cp_path = "/web/action"
1070 @openerpweb.jsonrequest
1071 def load(self, req, action_id):
1072 Actions = req.session.model('ir.actions.actions')
1074 context = req.session.eval_context(req.context)
1075 action_type = Actions.read([action_id], ['type'], context)
1078 if action_type[0]['type'] == 'ir.actions.report.xml':
1079 ctx.update({'bin_size': True})
1081 action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1083 value = clean_action(req, action[0])
1084 return {'result': value}
1086 @openerpweb.jsonrequest
1087 def run(self, req, action_id):
1088 return clean_action(req, req.session.model('ir.actions.server').run(
1089 [action_id], req.session.eval_context(req.context)))
1092 _cp_path = "/web/export"
1094 @openerpweb.jsonrequest
1095 def formats(self, req):
1096 """ Returns all valid export formats
1098 :returns: for each export format, a pair of identifier and printable name
1099 :rtype: [(str, str)]
1103 for path, controller in openerpweb.controllers_path.iteritems()
1104 if path.startswith(self._cp_path)
1105 if hasattr(controller, 'fmt')
1106 ], key=operator.itemgetter(1))
1108 def fields_get(self, req, model):
1109 Model = req.session.model(model)
1110 fields = Model.fields_get(False, req.session.eval_context(req.context))
1113 @openerpweb.jsonrequest
1114 def get_fields(self, req, model, prefix='', parent_name= '',
1115 import_compat=True, parent_field_type=None):
1117 if import_compat and parent_field_type == "many2one":
1120 fields = self.fields_get(req, model)
1121 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1123 fields_sequence = sorted(fields.iteritems(),
1124 key=lambda field: field[1].get('string', ''))
1127 for field_name, field in fields_sequence:
1128 if import_compat and field.get('readonly'):
1129 # If none of the field's states unsets readonly, skip the field
1130 if all(dict(attrs).get('readonly', True)
1131 for attrs in field.get('states', {}).values()):
1134 id = prefix + (prefix and '/'or '') + field_name
1135 name = parent_name + (parent_name and '/' or '') + field['string']
1136 record = {'id': id, 'string': name,
1137 'value': id, 'children': False,
1138 'field_type': field.get('type'),
1139 'required': field.get('required')}
1140 records.append(record)
1142 if len(name.split('/')) < 3 and 'relation' in field:
1143 ref = field.pop('relation')
1144 record['value'] += '/id'
1145 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1147 if not import_compat or field['type'] == 'one2many':
1148 # m2m field in import_compat is childless
1149 record['children'] = True
1153 @openerpweb.jsonrequest
1154 def namelist(self,req, model, export_id):
1155 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1156 export = req.session.model("ir.exports").read([export_id])[0]
1157 export_fields_list = req.session.model("ir.exports.line").read(
1158 export['export_fields'])
1160 fields_data = self.fields_info(
1161 req, model, map(operator.itemgetter('name'), export_fields_list))
1164 {'name': field['name'], 'label': fields_data[field['name']]}
1165 for field in export_fields_list
1168 def fields_info(self, req, model, export_fields):
1170 fields = self.fields_get(req, model)
1171 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1173 # To make fields retrieval more efficient, fetch all sub-fields of a
1174 # given field at the same time. Because the order in the export list is
1175 # arbitrary, this requires ordering all sub-fields of a given field
1176 # together so they can be fetched at the same time
1178 # Works the following way:
1179 # * sort the list of fields to export, the default sorting order will
1180 # put the field itself (if present, for xmlid) and all of its
1181 # sub-fields right after it
1182 # * then, group on: the first field of the path (which is the same for
1183 # a field and for its subfields and the length of splitting on the
1184 # first '/', which basically means grouping the field on one side and
1185 # all of the subfields on the other. This way, we have the field (for
1186 # the xmlid) with length 1, and all of the subfields with the same
1187 # base but a length "flag" of 2
1188 # * if we have a normal field (length 1), just add it to the info
1189 # mapping (with its string) as-is
1190 # * otherwise, recursively call fields_info via graft_subfields.
1191 # all graft_subfields does is take the result of fields_info (on the
1192 # field's model) and prepend the current base (current field), which
1193 # rebuilds the whole sub-tree for the field
1195 # result: because we're not fetching the fields_get for half the
1196 # database models, fetching a namelist with a dozen fields (including
1197 # relational data) falls from ~6s to ~300ms (on the leads model).
1198 # export lists with no sub-fields (e.g. import_compatible lists with
1199 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1200 # there's a single fields_get to execute)
1201 for (base, length), subfields in itertools.groupby(
1202 sorted(export_fields),
1203 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1204 subfields = list(subfields)
1206 # subfields is a seq of $base/*rest, and not loaded yet
1207 info.update(self.graft_subfields(
1208 req, fields[base]['relation'], base, fields[base]['string'],
1212 info[base] = fields[base]['string']
1216 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1217 export_fields = [field.split('/', 1)[1] for field in fields]
1219 (prefix + '/' + k, prefix_string + '/' + v)
1220 for k, v in self.fields_info(req, model, export_fields).iteritems())
1222 #noinspection PyPropertyDefinition
1224 def content_type(self):
1225 """ Provides the format's content type """
1226 raise NotImplementedError()
1228 def filename(self, base):
1229 """ Creates a valid filename for the format (with extension) from the
1230 provided base name (exension-less)
1232 raise NotImplementedError()
1234 def from_data(self, fields, rows):
1235 """ Conversion method from OpenERP's export data to whatever the
1236 current export class outputs
1238 :params list fields: a list of fields to export
1239 :params list rows: a list of records to export
1243 raise NotImplementedError()
1245 @openerpweb.httprequest
1246 def index(self, req, data, token):
1247 model, fields, ids, domain, import_compat = \
1248 operator.itemgetter('model', 'fields', 'ids', 'domain',
1250 simplejson.loads(data))
1252 context = req.session.eval_context(req.context)
1253 Model = req.session.model(model)
1254 ids = ids or Model.search(domain, context=context)
1256 field_names = map(operator.itemgetter('name'), fields)
1257 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1260 columns_headers = field_names
1262 columns_headers = [val['label'].strip() for val in fields]
1265 return req.make_response(self.from_data(columns_headers, import_data),
1266 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1267 ('Content-Type', self.content_type)],
1268 cookies={'fileToken': int(token)})
1270 class CSVExport(Export):
1271 _cp_path = '/web/export/csv'
1272 fmt = ('csv', 'CSV')
1275 def content_type(self):
1276 return 'text/csv;charset=utf8'
1278 def filename(self, base):
1279 return base + '.csv'
1281 def from_data(self, fields, rows):
1283 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1285 writer.writerow(fields)
1290 if isinstance(d, basestring):
1291 d = d.replace('\n',' ').replace('\t',' ')
1293 d = d.encode('utf-8')
1296 if d is False: d = None
1298 writer.writerow(row)
1305 class ExcelExport(Export):
1306 _cp_path = '/web/export/xls'
1307 fmt = ('xls', 'Excel')
1310 def content_type(self):
1311 return 'application/vnd.ms-excel'
1313 def filename(self, base):
1314 return base + '.xls'
1316 def from_data(self, fields, rows):
1319 workbook = xlwt.Workbook()
1320 worksheet = workbook.add_sheet('Sheet 1')
1322 for i, fieldname in enumerate(fields):
1323 worksheet.write(0, i, str(fieldname))
1324 worksheet.col(i).width = 8000 # around 220 pixels
1326 style = xlwt.easyxf('align: wrap yes')
1328 for row_index, row in enumerate(rows):
1329 for cell_index, cell_value in enumerate(row):
1330 if isinstance(cell_value, basestring):
1331 cell_value = re.sub("\r", " ", cell_value)
1332 worksheet.write(row_index + 1, cell_index, cell_value, style)
1341 class Reports(View):
1342 _cp_path = "/web/report"
1343 POLLING_DELAY = 0.25
1345 'doc': 'application/vnd.ms-word',
1346 'html': 'text/html',
1347 'odt': 'application/vnd.oasis.opendocument.text',
1348 'pdf': 'application/pdf',
1349 'sxw': 'application/vnd.sun.xml.writer',
1350 'xls': 'application/vnd.ms-excel',
1353 @openerpweb.httprequest
1354 def index(self, req, action, token):
1355 action = simplejson.loads(action)
1357 report_srv = req.session.proxy("report")
1358 context = req.session.eval_context(
1359 web.common.nonliterals.CompoundContext(
1360 req.context or {}, action[ "context"]))
1363 report_ids = context["active_ids"]
1364 if 'report_type' in action:
1365 report_data['report_type'] = action['report_type']
1366 if 'datas' in action:
1367 if 'form' in action['datas']:
1368 report_data['form'] = action['datas']['form']
1369 if 'ids' in action['datas']:
1370 report_ids = action['datas']['ids']
1372 report_id = report_srv.report(
1373 req.session._db, req.session._uid, req.session._password,
1374 action["report_name"], report_ids,
1375 report_data, context)
1377 report_struct = None
1379 report_struct = report_srv.report_get(
1380 req.session._db, req.session._uid, req.session._password, report_id)
1381 if report_struct["state"]:
1384 time.sleep(self.POLLING_DELAY)
1386 report = base64.b64decode(report_struct['result'])
1387 if report_struct.get('code') == 'zlib':
1388 report = zlib.decompress(report)
1389 report_mimetype = self.TYPES_MAPPING.get(
1390 report_struct['format'], 'octet-stream')
1391 return req.make_response(report,
1393 ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1394 ('Content-Type', report_mimetype),
1395 ('Content-Length', len(report))],
1396 cookies={'fileToken': int(token)})
1399 _cp_path = "/web/import"
1401 def fields_get(self, req, model):
1402 Model = req.session.model(model)
1403 fields = Model.fields_get(False, req.session.eval_context(req.context))
1406 @openerpweb.httprequest
1407 def detect_data(self, req, csvfile, csvsep, csvdel, csvcode, jsonp):
1409 data = list(csv.reader(
1410 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1411 except csv.Error, e:
1413 return '<script>window.top.%s(%s);</script>' % (
1414 jsonp, simplejson.dumps({'error': {
1415 'message': 'Error parsing CSV file: %s' % e,
1416 # decodes each byte to a unicode character, which may or
1417 # may not be printable, but decoding will succeed.
1418 # Otherwise simplejson will try to decode the `str` using
1419 # utf-8, which is very likely to blow up on characters out
1420 # of the ascii range (in range [128, 256))
1421 'preview': csvfile.read(200).decode('iso-8859-1')}}))
1424 return '<script>window.top.%s(%s);</script>' % (
1425 jsonp, simplejson.dumps(
1426 {'records': data[:10]}, encoding=csvcode))
1427 except UnicodeDecodeError:
1428 return '<script>window.top.%s(%s);</script>' % (
1429 jsonp, simplejson.dumps({
1430 'message': u"Failed to decode CSV file using encoding %s, "
1431 u"try switching to a different encoding" % csvcode
1434 @openerpweb.httprequest
1435 def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1437 modle_obj = req.session.model(model)
1438 skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1439 simplejson.loads(meta))
1442 if not (csvdel and len(csvdel) == 1):
1443 error = u"The CSV delimiter must be a single character"
1445 if not indices and fields:
1446 error = u"You must select at least one field to import"
1449 return '<script>window.top.%s(%s);</script>' % (
1450 jsonp, simplejson.dumps({'error': {'message': error}}))
1452 # skip ignored records
1453 data_record = itertools.islice(
1454 csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1457 # if only one index, itemgetter will return an atom rather than a tuple
1458 if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1459 else: mapper = operator.itemgetter(*indices)
1464 # decode each data row
1466 [record.decode(csvcode) for record in row]
1467 for row in itertools.imap(mapper, data_record)
1468 # don't insert completely empty rows (can happen due to fields
1469 # filtering in case of e.g. o2m content rows)
1472 except UnicodeDecodeError:
1473 error = u"Failed to decode CSV file using encoding %s" % csvcode
1474 except csv.Error, e:
1475 error = u"Could not process CSV file: %s" % e
1477 # If the file contains nothing,
1479 error = u"File to import is empty"
1481 return '<script>window.top.%s(%s);</script>' % (
1482 jsonp, simplejson.dumps({'error': {'message': error}}))
1485 (code, record, message, _nope) = modle_obj.import_data(
1486 fields, data, 'init', '', False,
1487 req.session.eval_context(req.context))
1488 except xmlrpclib.Fault, e:
1489 error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1490 return '<script>window.top.%s(%s);</script>' % (
1491 jsonp, simplejson.dumps({'error':error}))
1494 return '<script>window.top.%s(%s);</script>' % (
1495 jsonp, simplejson.dumps({'success':True}))
1497 msg = u"Error during import: %s\n\nTrying to import record %r" % (
1499 return '<script>window.top.%s(%s);</script>' % (
1500 jsonp, simplejson.dumps({'error': {'message':msg}}))