1 # -*- coding: utf-8 -*-
15 from xml.etree import ElementTree
16 from cStringIO import StringIO
18 import web.common.dispatch as openerpweb
20 import web.common.nonliterals
21 openerpweb.ast = web.common.ast
22 openerpweb.nonliterals = web.common.nonliterals
24 from babel.messages.pofile import read_po
26 _REPORT_POLLER_DELAY = 0.05
28 # Should move to openerpweb.Xml2Json
31 # Simple and straightforward XML-to-JSON converter in Python
34 # URL: http://code.google.com/p/xml2json-direct/
36 def convert_to_json(s):
37 return simplejson.dumps(
38 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
41 def convert_to_structure(s):
42 root = ElementTree.fromstring(s)
43 return Xml2Json.convert_element(root)
46 def convert_element(el, skip_whitespaces=True):
49 ns, name = el.tag.rsplit("}", 1)
51 res["namespace"] = ns[1:]
55 for k, v in el.items():
58 if el.text and (not skip_whitespaces or el.text.strip() != ''):
61 kids.append(Xml2Json.convert_element(kid))
62 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
64 res["children"] = kids
67 #----------------------------------------------------------
68 # OpenERP Web web Controllers
69 #----------------------------------------------------------
71 def manifest_glob(addons_path, addons, key):
74 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
75 for pattern in globlist:
76 for path in glob.glob(os.path.join(addons_path, addon, pattern)):
77 files.append(path[len(addons_path):])
80 def concat_files(addons_path, file_list):
81 """ Concatenate file content
82 return (concat,timestamp)
83 concat: concatenation of file content
84 timestamp: max(os.path.getmtime of file_list)
89 fname = os.path.join(addons_path, i[1:])
90 ftime = os.path.getmtime(fname)
91 if ftime > files_timestamp:
92 files_timestamp = ftime
93 files_content.append(open(fname).read())
94 files_concat = "".join(files_content)
95 return files_concat,files_timestamp
97 home_template = textwrap.dedent("""<!DOCTYPE html>
98 <html style="height: 100%%">
100 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
101 <title>OpenERP</title>
102 <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
105 <script type="text/javascript">
107 QWeb = new QWeb2.Engine();
108 var c = new openerp.init();
109 var wc = new c.web.WebClient("oe");
114 <body id="oe" class="openerp"></body>
117 class WebClient(openerpweb.Controller):
118 _cp_path = "/web/webclient"
120 @openerpweb.jsonrequest
121 def csslist(self, req, mods='web'):
122 return manifest_glob(req.config.addons_path, mods.split(','), 'css')
124 @openerpweb.jsonrequest
125 def jslist(self, req, mods='web'):
126 return manifest_glob(req.config.addons_path, mods.split(','), 'js')
128 @openerpweb.httprequest
129 def css(self, req, mods='web'):
130 files = manifest_glob(req.config.addons_path, mods.split(','), 'css')
131 content,timestamp = concat_files(req.config.addons_path, files)
132 # TODO request set the Date of last modif and Etag
133 return req.make_response(content, [('Content-Type', 'text/css')])
135 @openerpweb.httprequest
136 def js(self, req, mods='web'):
137 files = manifest_glob(req.config.addons_path, mods.split(','), 'js')
138 content,timestamp = concat_files(req.config.addons_path, files)
139 # TODO request set the Date of last modif and Etag
140 return req.make_response(content, [('Content-Type', 'application/javascript')])
142 @openerpweb.httprequest
143 def home(self, req, s_action=None, **kw):
145 jslist = ['/web/webclient/js']
147 jslist = manifest_glob(req.config.addons_path, ['web'], 'js')
148 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
151 csslist = ['/web/webclient/css']
153 csslist = manifest_glob(req.config.addons_path, ['web'], 'css')
154 css = "\n ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
155 r = home_template % {
161 @openerpweb.jsonrequest
162 def translations(self, req, mods, lang):
163 lang_model = req.session.model('res.lang')
164 ids = lang_model.search([("code", "=", lang)])
166 lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
167 "grouping", "decimal_point", "thousands_sep"])
171 if lang.count("_") > 0:
175 langs = lang.split(separator)
176 langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
179 for addon_name in mods:
180 transl = {"messages":[]}
181 transs[addon_name] = transl
183 f_name = os.path.join(req.config.addons_path, addon_name, "po", l + ".po")
184 if not os.path.exists(f_name):
187 with open(f_name) as t_file:
192 if x.id and x.string:
193 transl["messages"].append({'id': x.id, 'string': x.string})
194 return {"modules": transs,
195 "lang_parameters": lang_obj}
197 @openerpweb.jsonrequest
198 def version_info(self, req):
200 "version": webrelease.version
203 class Database(openerpweb.Controller):
204 _cp_path = "/web/database"
206 @openerpweb.jsonrequest
207 def get_list(self, req):
208 proxy = req.session.proxy("db")
210 h = req.httprequest.headers['Host'].split(':')[0]
212 r = req.config.dbfilter.replace('%h', h).replace('%d', d)
213 dbs = [i for i in dbs if re.match(r, i)]
214 return {"db_list": dbs}
216 @openerpweb.jsonrequest
217 def progress(self, req, password, id):
218 return req.session.proxy('db').get_progress(password, id)
220 @openerpweb.jsonrequest
221 def create(self, req, fields):
223 params = dict(map(operator.itemgetter('name', 'value'), fields))
225 params['super_admin_pwd'],
227 bool(params.get('demo_data')),
229 params['create_admin_pwd']
233 return req.session.proxy("db").create(*create_attrs)
234 except xmlrpclib.Fault, e:
235 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
236 return {'error': e.faultCode, 'title': 'Create Database'}
237 return {'error': 'Could not create database !', 'title': 'Create Database'}
239 @openerpweb.jsonrequest
240 def drop(self, req, fields):
241 password, db = operator.itemgetter(
242 'drop_pwd', 'drop_db')(
243 dict(map(operator.itemgetter('name', 'value'), fields)))
246 return req.session.proxy("db").drop(password, db)
247 except xmlrpclib.Fault, e:
248 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
249 return {'error': e.faultCode, 'title': 'Drop Database'}
250 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
252 @openerpweb.httprequest
253 def backup(self, req, backup_db, backup_pwd, token):
255 db_dump = base64.decodestring(
256 req.session.proxy("db").dump(backup_pwd, backup_db))
257 return req.make_response(db_dump,
258 [('Content-Type', 'application/octet-stream; charset=binary'),
259 ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
260 {'fileToken': int(token)}
262 except xmlrpclib.Fault, e:
263 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
264 return 'Backup Database|' + e.faultCode
265 return 'Backup Database|Could not generate database backup'
267 @openerpweb.httprequest
268 def restore(self, req, db_file, restore_pwd, new_db):
270 data = base64.encodestring(db_file.file.read())
271 req.session.proxy("db").restore(restore_pwd, new_db, data)
273 except xmlrpclib.Fault, e:
274 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
275 raise Exception("AccessDenied")
277 @openerpweb.jsonrequest
278 def change_password(self, req, fields):
279 old_password, new_password = operator.itemgetter(
280 'old_pwd', 'new_pwd')(
281 dict(map(operator.itemgetter('name', 'value'), fields)))
283 return req.session.proxy("db").change_admin_password(old_password, new_password)
284 except xmlrpclib.Fault, e:
285 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
286 return {'error': e.faultCode, 'title': 'Change Password'}
287 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
289 class Session(openerpweb.Controller):
290 _cp_path = "/web/session"
292 @openerpweb.jsonrequest
293 def login(self, req, db, login, password):
294 req.session.login(db, login, password)
295 ctx = req.session.get_context()
298 "session_id": req.session_id,
299 "uid": req.session._uid,
302 @openerpweb.jsonrequest
303 def change_password (self,req,fields):
304 old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
305 dict(map(operator.itemgetter('name', 'value'), fields)))
306 if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
307 return {'error':'All passwords have to be filled.','title': 'Change Password'}
308 if new_password != confirm_password:
309 return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
311 if req.session.model('res.users').change_password(
312 old_password, new_password):
313 return {'new_password':new_password}
315 return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
316 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
317 @openerpweb.jsonrequest
318 def sc_list(self, req):
319 return req.session.model('ir.ui.view_sc').get_sc(
320 req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
322 @openerpweb.jsonrequest
323 def get_lang_list(self, req):
326 'lang_list': (req.session.proxy("db").list_lang() or []),
330 return {"error": e, "title": "Languages"}
332 @openerpweb.jsonrequest
333 def modules(self, req):
334 # TODO query server for installed web modules
336 for name, manifest in openerpweb.addons_manifest.items():
337 if name != 'web' and manifest.get('active', True):
341 @openerpweb.jsonrequest
342 def eval_domain_and_context(self, req, contexts, domains,
344 """ Evaluates sequences of domains and contexts, composing them into
345 a single context, domain or group_by sequence.
347 :param list contexts: list of contexts to merge together. Contexts are
348 evaluated in sequence, all previous contexts
349 are part of their own evaluation context
350 (starting at the session context).
351 :param list domains: list of domains to merge together. Domains are
352 evaluated in sequence and appended to one another
353 (implicit AND), their evaluation domain is the
354 result of merging all contexts.
355 :param list group_by_seq: list of domains (which may be in a different
356 order than the ``contexts`` parameter),
357 evaluated in sequence, their ``'group_by'``
358 key is extracted if they have one.
363 the global context created by merging all of
367 the concatenation of all domains
370 a list of fields to group by, potentially empty (in which case
371 no group by should be performed)
373 context, domain = eval_context_and_domain(req.session,
374 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
375 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
377 group_by_sequence = []
378 for candidate in (group_by_seq or []):
379 ctx = req.session.eval_context(candidate, context)
380 group_by = ctx.get('group_by')
383 elif isinstance(group_by, basestring):
384 group_by_sequence.append(group_by)
386 group_by_sequence.extend(group_by)
391 'group_by': group_by_sequence
394 @openerpweb.jsonrequest
395 def save_session_action(self, req, the_action):
397 This method store an action object in the session object and returns an integer
398 identifying that action. The method get_session_action() can be used to get
401 :param the_action: The action to save in the session.
402 :type the_action: anything
403 :return: A key identifying the saved action.
406 saved_actions = req.httpsession.get('saved_actions')
407 if not saved_actions:
408 saved_actions = {"next":0, "actions":{}}
409 req.httpsession['saved_actions'] = saved_actions
410 # we don't allow more than 10 stored actions
411 if len(saved_actions["actions"]) >= 10:
412 del saved_actions["actions"][min(saved_actions["actions"].keys())]
413 key = saved_actions["next"]
414 saved_actions["actions"][key] = the_action
415 saved_actions["next"] = key + 1
418 @openerpweb.jsonrequest
419 def get_session_action(self, req, key):
421 Gets back a previously saved action. This method can return None if the action
422 was saved since too much time (this case should be handled in a smart way).
424 :param key: The key given by save_session_action()
426 :return: The saved action or None.
429 saved_actions = req.httpsession.get('saved_actions')
430 if not saved_actions:
432 return saved_actions["actions"].get(key)
434 @openerpweb.jsonrequest
435 def check(self, req):
436 req.session.assert_valid()
439 def eval_context_and_domain(session, context, domain=None):
440 e_context = session.eval_context(context)
441 # should we give the evaluated context as an evaluation context to the domain?
442 e_domain = session.eval_domain(domain or [])
444 return e_context, e_domain
446 def load_actions_from_ir_values(req, key, key2, models, meta):
447 context = req.session.eval_context(req.context)
448 Values = req.session.model('ir.values')
449 actions = Values.get(key, key2, models, meta, context)
451 return [(id, name, clean_action(req, action))
452 for id, name, action in actions]
454 def clean_action(req, action):
455 action.setdefault('flags', {})
456 if action['type'] != 'ir.actions.act_window':
459 context = req.session.eval_context(req.context)
460 eval_ctx = req.session.evaluation_context(context)
462 # values come from the server, we can just eval them
463 if isinstance(action.get('context'), basestring):
464 action['context'] = eval( action['context'], eval_ctx ) or {}
466 if isinstance(action.get('domain'), basestring):
467 action['domain'] = eval( action['domain'], eval_ctx ) or []
469 return fix_view_modes(action)
471 # I think generate_views,fix_view_modes should go into js ActionManager
472 def generate_views(action):
474 While the server generates a sequence called "views" computing dependencies
475 between a bunch of stuff for views coming directly from the database
476 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
477 to return custom view dictionaries generated on the fly.
479 In that case, there is no ``views`` key available on the action.
481 Since the web client relies on ``action['views']``, generate it here from
482 ``view_mode`` and ``view_id``.
484 Currently handles two different cases:
486 * no view_id, multiple view_mode
487 * single view_id, single view_mode
489 :param dict action: action descriptor dictionary to generate a views key for
491 view_id = action.get('view_id', False)
492 if isinstance(view_id, (list, tuple)):
495 # providing at least one view mode is a requirement, not an option
496 view_modes = action['view_mode'].split(',')
498 if len(view_modes) > 1:
500 raise ValueError('Non-db action dictionaries should provide '
501 'either multiple view modes or a single view '
502 'mode and an optional view id.\n\n Got view '
503 'modes %r and view id %r for action %r' % (
504 view_modes, view_id, action))
505 action['views'] = [(False, mode) for mode in view_modes]
507 action['views'] = [(view_id, view_modes[0])]
509 def fix_view_modes(action):
510 """ For historical reasons, OpenERP has weird dealings in relation to
511 view_mode and the view_type attribute (on window actions):
513 * one of the view modes is ``tree``, which stands for both list views
515 * the choice is made by checking ``view_type``, which is either
516 ``form`` for a list view or ``tree`` for an actual tree view
518 This methods simply folds the view_type into view_mode by adding a
519 new view mode ``list`` which is the result of the ``tree`` view_mode
520 in conjunction with the ``form`` view_type.
522 TODO: this should go into the doc, some kind of "peculiarities" section
524 :param dict action: an action descriptor
525 :returns: nothing, the action is modified in place
527 if 'views' not in action:
528 generate_views(action)
530 if action.pop('view_type') != 'form':
534 [id, mode if mode != 'tree' else 'list']
535 for id, mode in action['views']
540 class Menu(openerpweb.Controller):
541 _cp_path = "/web/menu"
543 @openerpweb.jsonrequest
545 return {'data': self.do_load(req)}
547 def do_load(self, req):
548 """ Loads all menu items (all applications and their sub-menus).
550 :param req: A request object, with an OpenERP session attribute
551 :type req: < session -> OpenERPSession >
552 :return: the menu root
553 :rtype: dict('children': menu_nodes)
555 Menus = req.session.model('ir.ui.menu')
556 # menus are loaded fully unlike a regular tree view, cause there are
557 # less than 512 items
558 context = req.session.eval_context(req.context)
559 menu_ids = Menus.search([], 0, False, False, context)
560 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
561 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
562 menu_items.append(menu_root)
564 # make a tree using parent_id
565 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
566 for menu_item in menu_items:
567 if menu_item['parent_id']:
568 parent = menu_item['parent_id'][0]
571 if parent in menu_items_map:
572 menu_items_map[parent].setdefault(
573 'children', []).append(menu_item)
575 # sort by sequence a tree using parent_id
576 for menu_item in menu_items:
577 menu_item.setdefault('children', []).sort(
578 key=lambda x:x["sequence"])
582 @openerpweb.jsonrequest
583 def action(self, req, menu_id):
584 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
585 [('ir.ui.menu', menu_id)], False)
586 return {"action": actions}
588 class DataSet(openerpweb.Controller):
589 _cp_path = "/web/dataset"
591 @openerpweb.jsonrequest
592 def fields(self, req, model):
593 return {'fields': req.session.model(model).fields_get(False,
594 req.session.eval_context(req.context))}
596 @openerpweb.jsonrequest
597 def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
598 return self.do_search_read(req, model, fields, offset, limit, domain, sort)
599 def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
601 """ Performs a search() followed by a read() (if needed) using the
602 provided search criteria
604 :param req: a JSON-RPC request object
605 :type req: openerpweb.JsonRequest
606 :param str model: the name of the model to search on
607 :param fields: a list of the fields to return in the result records
609 :param int offset: from which index should the results start being returned
610 :param int limit: the maximum number of records to return
611 :param list domain: the search domain for the query
612 :param list sort: sorting directives
613 :returns: A structure (dict) with two keys: ids (all the ids matching
614 the (domain, context) pair) and records (paginated records
615 matching fields selection set)
618 Model = req.session.model(model)
620 context, domain = eval_context_and_domain(
621 req.session, req.context, domain)
623 ids = Model.search(domain, 0, False, sort or False, context)
624 # need to fill the dataset with all ids for the (domain, context) pair,
625 # so search un-paginated and paginate manually before reading
626 paginated_ids = ids[offset:(offset + limit if limit else None)]
627 if fields and fields == ['id']:
628 # shortcut read if we only want the ids
631 'records': map(lambda id: {'id': id}, paginated_ids)
634 records = Model.read(paginated_ids, fields or False, context)
635 records.sort(key=lambda obj: ids.index(obj['id']))
642 @openerpweb.jsonrequest
643 def read(self, req, model, ids, fields=False):
644 return self.do_search_read(req, model, ids, fields)
646 @openerpweb.jsonrequest
647 def get(self, req, model, ids, fields=False):
648 return self.do_get(req, model, ids, fields)
650 def do_get(self, req, model, ids, fields=False):
651 """ Fetches and returns the records of the model ``model`` whose ids
654 The results are in the same order as the inputs, but elements may be
655 missing (if there is no record left for the id)
657 :param req: the JSON-RPC2 request object
658 :type req: openerpweb.JsonRequest
659 :param model: the model to read from
661 :param ids: a list of identifiers
663 :param fields: a list of fields to fetch, ``False`` or empty to fetch
664 all fields in the model
665 :type fields: list | False
666 :returns: a list of records, in the same order as the list of ids
669 Model = req.session.model(model)
670 records = Model.read(ids, fields, req.session.eval_context(req.context))
672 record_map = dict((record['id'], record) for record in records)
674 return [record_map[id] for id in ids if record_map.get(id)]
676 @openerpweb.jsonrequest
677 def load(self, req, model, id, fields):
678 m = req.session.model(model)
680 r = m.read([id], False, req.session.eval_context(req.context))
683 return {'value': value}
685 @openerpweb.jsonrequest
686 def create(self, req, model, data):
687 m = req.session.model(model)
688 r = m.create(data, req.session.eval_context(req.context))
691 @openerpweb.jsonrequest
692 def save(self, req, model, id, data):
693 m = req.session.model(model)
694 r = m.write([id], data, req.session.eval_context(req.context))
697 @openerpweb.jsonrequest
698 def unlink(self, req, model, ids=()):
699 Model = req.session.model(model)
700 return Model.unlink(ids, req.session.eval_context(req.context))
702 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
703 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
704 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
705 c, d = eval_context_and_domain(req.session, context, domain)
706 if domain_id and len(args) - 1 >= domain_id:
708 if context_id and len(args) - 1 >= context_id:
711 for i in xrange(len(args)):
712 if isinstance(args[i], web.common.nonliterals.BaseContext):
713 args[i] = req.session.eval_context(args[i])
714 if isinstance(args[i], web.common.nonliterals.BaseDomain):
715 args[i] = req.session.eval_domain(args[i])
717 return getattr(req.session.model(model), method)(*args)
719 @openerpweb.jsonrequest
720 def call(self, req, model, method, args, domain_id=None, context_id=None):
721 return self.call_common(req, model, method, args, domain_id, context_id)
723 @openerpweb.jsonrequest
724 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
725 action = self.call_common(req, model, method, args, domain_id, context_id)
726 if isinstance(action, dict) and action.get('type') != '':
727 return {'result': clean_action(req, action)}
728 return {'result': False}
730 @openerpweb.jsonrequest
731 def exec_workflow(self, req, model, id, signal):
732 r = req.session.exec_workflow(model, id, signal)
735 @openerpweb.jsonrequest
736 def default_get(self, req, model, fields):
737 Model = req.session.model(model)
738 return Model.default_get(fields, req.session.eval_context(req.context))
740 @openerpweb.jsonrequest
741 def name_search(self, req, model, search_str, domain=[], context={}):
742 m = req.session.model(model)
743 r = m.name_search(search_str+'%', domain, '=ilike', context)
746 class DataGroup(openerpweb.Controller):
747 _cp_path = "/web/group"
748 @openerpweb.jsonrequest
749 def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
750 Model = req.session.model(model)
751 context, domain = eval_context_and_domain(req.session, req.context, domain)
753 return Model.read_group(
754 domain or [], fields, group_by_fields, 0, False,
755 dict(context, group_by=group_by_fields), sort or False)
757 class View(openerpweb.Controller):
758 _cp_path = "/web/view"
760 def fields_view_get(self, req, model, view_id, view_type,
761 transform=True, toolbar=False, submenu=False):
762 Model = req.session.model(model)
763 context = req.session.eval_context(req.context)
764 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
765 # todo fme?: check that we should pass the evaluated context here
766 self.process_view(req.session, fvg, context, transform)
769 def process_view(self, session, fvg, context, transform):
770 # depending on how it feels, xmlrpclib.ServerProxy can translate
771 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
772 # enjoy unicode strings which can not be trivially converted to
773 # strings, and it blows up during parsing.
775 # So ensure we fix this retardation by converting view xml back to
777 if isinstance(fvg['arch'], unicode):
778 arch = fvg['arch'].encode('utf-8')
783 evaluation_context = session.evaluation_context(context or {})
784 xml = self.transform_view(arch, session, evaluation_context)
786 xml = ElementTree.fromstring(arch)
787 fvg['arch'] = Xml2Json.convert_element(xml)
789 for field in fvg['fields'].itervalues():
790 if field.get('views'):
791 for view in field["views"].itervalues():
792 self.process_view(session, view, None, transform)
793 if field.get('domain'):
794 field["domain"] = self.parse_domain(field["domain"], session)
795 if field.get('context'):
796 field["context"] = self.parse_context(field["context"], session)
798 @openerpweb.jsonrequest
799 def add_custom(self, req, view_id, arch):
800 CustomView = req.session.model('ir.ui.view.custom')
802 'user_id': req.session._uid,
805 }, req.session.eval_context(req.context))
806 return {'result': True}
808 @openerpweb.jsonrequest
809 def undo_custom(self, req, view_id, reset=False):
810 CustomView = req.session.model('ir.ui.view.custom')
811 context = req.session.eval_context(req.context)
812 vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
813 0, False, False, context)
816 CustomView.unlink(vcustom, context)
818 CustomView.unlink([vcustom[0]], context)
819 return {'result': True}
820 return {'result': False}
822 def transform_view(self, view_string, session, context=None):
823 # transform nodes on the fly via iterparse, instead of
824 # doing it statically on the parsing result
825 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
827 for event, elem in parser:
831 self.parse_domains_and_contexts(elem, session)
834 def parse_domain(self, domain, session):
835 """ Parses an arbitrary string containing a domain, transforms it
836 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
838 :param domain: the domain to parse, if the domain is not a string it
839 is assumed to be a literal domain and is returned as-is
840 :param session: Current OpenERP session
841 :type session: openerpweb.openerpweb.OpenERPSession
843 if not isinstance(domain, (str, unicode)):
846 return openerpweb.ast.literal_eval(domain)
849 return openerpweb.nonliterals.Domain(session, domain)
851 def parse_context(self, context, session):
852 """ Parses an arbitrary string containing a context, transforms it
853 to either a literal context or a :class:`openerpweb.nonliterals.Context`
855 :param context: the context to parse, if the context is not a string it
856 is assumed to be a literal domain and is returned as-is
857 :param session: Current OpenERP session
858 :type session: openerpweb.openerpweb.OpenERPSession
860 if not isinstance(context, (str, unicode)):
863 return openerpweb.ast.literal_eval(context)
865 return openerpweb.nonliterals.Context(session, context)
867 def parse_domains_and_contexts(self, elem, session):
868 """ Converts domains and contexts from the view into Python objects,
869 either literals if they can be parsed by literal_eval or a special
870 placeholder object if the domain or context refers to free variables.
872 :param elem: the current node being parsed
873 :type param: xml.etree.ElementTree.Element
874 :param session: OpenERP session object, used to store and retrieve
876 :type session: openerpweb.openerpweb.OpenERPSession
878 for el in ['domain', 'filter_domain']:
879 domain = elem.get(el, '').strip()
881 elem.set(el, self.parse_domain(domain, session))
882 for el in ['context', 'default_get']:
883 context_string = elem.get(el, '').strip()
885 elem.set(el, self.parse_context(context_string, session))
887 class FormView(View):
888 _cp_path = "/web/formview"
890 @openerpweb.jsonrequest
891 def load(self, req, model, view_id, toolbar=False):
892 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
893 return {'fields_view': fields_view}
895 class ListView(View):
896 _cp_path = "/web/listview"
898 @openerpweb.jsonrequest
899 def load(self, req, model, view_id, toolbar=False):
900 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
901 return {'fields_view': fields_view}
903 def process_colors(self, view, row, context):
904 colors = view['arch']['attrs'].get('colors')
911 for pair in colors.split(';')
912 if eval(pair.split(':')[1], dict(context, **row))
917 elif len(color) == 1:
921 class SearchView(View):
922 _cp_path = "/web/searchview"
924 @openerpweb.jsonrequest
925 def load(self, req, model, view_id):
926 fields_view = self.fields_view_get(req, model, view_id, 'search')
927 return {'fields_view': fields_view}
929 @openerpweb.jsonrequest
930 def fields_get(self, req, model):
931 Model = req.session.model(model)
932 fields = Model.fields_get(False, req.session.eval_context(req.context))
933 for field in fields.values():
934 # shouldn't convert the views too?
935 if field.get('domain'):
936 field["domain"] = self.parse_domain(field["domain"], req.session)
937 if field.get('context'):
938 field["context"] = self.parse_domain(field["context"], req.session)
939 return {'fields': fields}
941 @openerpweb.jsonrequest
942 def get_filters(self, req, model):
943 Model = req.session.model("ir.filters")
944 filters = Model.get_filters(model)
945 for filter in filters:
946 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
947 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
950 @openerpweb.jsonrequest
951 def save_filter(self, req, model, name, context_to_save, domain):
952 Model = req.session.model("ir.filters")
953 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
954 ctx.session = req.session
956 domain = openerpweb.nonliterals.CompoundDomain(domain)
957 domain.session = req.session
958 domain = domain.evaluate()
959 uid = req.session._uid
960 context = req.session.eval_context(req.context)
961 to_return = Model.create_or_replace({"context": ctx,
969 class Binary(openerpweb.Controller):
970 _cp_path = "/web/binary"
972 @openerpweb.httprequest
973 def image(self, req, model, id, field, **kw):
974 Model = req.session.model(model)
975 context = req.session.eval_context(req.context)
979 res = Model.default_get([field], context).get(field, '')
981 res = Model.read([int(id)], [field], context)[0].get(field, '')
982 image_data = base64.decodestring(res)
983 except: # TODO: what's the exception here?
984 image_data = self.placeholder(req)
985 return req.make_response(image_data, [
986 ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
987 def placeholder(self, req):
988 return open(os.path.join(req.addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
990 @openerpweb.httprequest
991 def saveas(self, req, model, id, field, fieldname, **kw):
992 Model = req.session.model(model)
993 context = req.session.eval_context(req.context)
994 res = Model.read([int(id)], [field, fieldname], context)[0]
995 filecontent = res.get(field, '')
997 return req.not_found()
999 filename = '%s_%s' % (model.replace('.', '_'), id)
1001 filename = res.get(fieldname, '') or filename
1002 return req.make_response(filecontent,
1003 [('Content-Type', 'application/octet-stream'),
1004 ('Content-Disposition', 'attachment; filename=' + filename)])
1006 @openerpweb.httprequest
1007 def upload(self, req, callback, ufile=None):
1009 for key, val in req.httprequest.headers.iteritems():
1010 headers[key.lower()] = val
1011 size = int(headers.get('content-length', 0))
1012 # TODO: might be useful to have a configuration flag for max-length file uploads
1014 out = """<script language="javascript" type="text/javascript">
1015 var win = window.top.window,
1017 if (typeof(callback) === 'function') {
1018 callback.apply(this, %s);
1020 win.jQuery('#oe_notification', win.document).notify('create', {
1021 title: "Ajax File Upload",
1022 text: "Could not find callback"
1026 data = ufile.file.read()
1027 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
1028 except Exception, e:
1029 args = [False, e.message]
1030 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1032 @openerpweb.httprequest
1033 def upload_attachment(self, req, callback, model, id, ufile=None):
1034 context = req.session.eval_context(req.context)
1035 Model = req.session.model('ir.attachment')
1037 out = """<script language="javascript" type="text/javascript">
1038 var win = window.top.window,
1040 if (typeof(callback) === 'function') {
1041 callback.call(this, %s);
1044 attachment_id = Model.create({
1045 'name': ufile.filename,
1046 'datas': web64.encodestring(ufile.file.read()),
1051 'filename': ufile.filename,
1054 except Exception, e:
1055 args = { 'error': e.message }
1056 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1058 class Action(openerpweb.Controller):
1059 _cp_path = "/web/action"
1061 @openerpweb.jsonrequest
1062 def load(self, req, action_id):
1063 Actions = req.session.model('ir.actions.actions')
1065 context = req.session.eval_context(req.context)
1066 action_type = Actions.read([action_id], ['type'], context)
1068 action = req.session.model(action_type[0]['type']).read([action_id], False,
1071 value = clean_action(req, action[0])
1072 return {'result': value}
1074 @openerpweb.jsonrequest
1075 def run(self, req, action_id):
1076 return clean_action(req, req.session.model('ir.actions.server').run(
1077 [action_id], req.session.eval_context(req.context)))
1079 class TreeView(View):
1080 _cp_path = "/web/treeview"
1082 @openerpweb.jsonrequest
1083 def load(self, req, model, view_id, toolbar=False):
1084 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1086 @openerpweb.jsonrequest
1087 def action(self, req, model, id):
1088 return load_actions_from_ir_values(
1089 req,'action', 'tree_but_open',[(model, id)],
1093 _cp_path = "/web/export"
1095 @openerpweb.jsonrequest
1096 def formats(self, req):
1097 """ Returns all valid export formats
1099 :returns: for each export format, a pair of identifier and printable name
1100 :rtype: [(str, str)]
1104 for path, controller in openerpweb.controllers_path.iteritems()
1105 if path.startswith(self._cp_path)
1106 if hasattr(controller, 'fmt')
1107 ], key=operator.itemgetter(1))
1109 def fields_get(self, req, model):
1110 Model = req.session.model(model)
1111 fields = Model.fields_get(False, req.session.eval_context(req.context))
1114 @openerpweb.jsonrequest
1115 def get_fields(self, req, model, prefix='', parent_name= '',
1116 import_compat=True, parent_field_type=None):
1118 if import_compat and parent_field_type == "many2one":
1121 fields = self.fields_get(req, model)
1122 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1124 fields_sequence = sorted(fields.iteritems(),
1125 key=lambda field: field[1].get('string', ''))
1128 for field_name, field in fields_sequence:
1129 if import_compat and field.get('readonly'):
1130 # If none of the field's states unsets readonly, skip the field
1131 if all(dict(attrs).get('readonly', True)
1132 for attrs in field.get('states', {}).values()):
1135 id = prefix + (prefix and '/'or '') + field_name
1136 name = parent_name + (parent_name and '/' or '') + field['string']
1137 record = {'id': id, 'string': name,
1138 'value': id, 'children': False,
1139 'field_type': field.get('type'),
1140 'required': field.get('required')}
1141 records.append(record)
1143 if len(name.split('/')) < 3 and 'relation' in field:
1144 ref = field.pop('relation')
1145 record['value'] += '/id'
1146 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1148 if not import_compat or field['type'] == 'one2many':
1149 # m2m field in import_compat is childless
1150 record['children'] = True
1154 @openerpweb.jsonrequest
1155 def namelist(self,req, model, export_id):
1156 # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1157 export = req.session.model("ir.exports").read([export_id])[0]
1158 export_fields_list = req.session.model("ir.exports.line").read(
1159 export['export_fields'])
1161 fields_data = self.fields_info(
1162 req, model, map(operator.itemgetter('name'), export_fields_list))
1165 {'name': field['name'], 'label': fields_data[field['name']]}
1166 for field in export_fields_list
1169 def fields_info(self, req, model, export_fields):
1171 fields = self.fields_get(req, model)
1172 fields['.id'] = fields.pop('id') if 'id' in fields else {'string': 'ID'}
1174 # To make fields retrieval more efficient, fetch all sub-fields of a
1175 # given field at the same time. Because the order in the export list is
1176 # arbitrary, this requires ordering all sub-fields of a given field
1177 # together so they can be fetched at the same time
1179 # Works the following way:
1180 # * sort the list of fields to export, the default sorting order will
1181 # put the field itself (if present, for xmlid) and all of its
1182 # sub-fields right after it
1183 # * then, group on: the first field of the path (which is the same for
1184 # a field and for its subfields and the length of splitting on the
1185 # first '/', which basically means grouping the field on one side and
1186 # all of the subfields on the other. This way, we have the field (for
1187 # the xmlid) with length 1, and all of the subfields with the same
1188 # base but a length "flag" of 2
1189 # * if we have a normal field (length 1), just add it to the info
1190 # mapping (with its string) as-is
1191 # * otherwise, recursively call fields_info via graft_subfields.
1192 # all graft_subfields does is take the result of fields_info (on the
1193 # field's model) and prepend the current base (current field), which
1194 # rebuilds the whole sub-tree for the field
1196 # result: because we're not fetching the fields_get for half the
1197 # database models, fetching a namelist with a dozen fields (including
1198 # relational data) falls from ~6s to ~300ms (on the leads model).
1199 # export lists with no sub-fields (e.g. import_compatible lists with
1200 # no o2m) are even more efficient (from the same 6s to ~170ms, as
1201 # there's a single fields_get to execute)
1202 for (base, length), subfields in itertools.groupby(
1203 sorted(export_fields),
1204 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1205 subfields = list(subfields)
1207 # subfields is a seq of $base/*rest, and not loaded yet
1208 info.update(self.graft_subfields(
1209 req, fields[base]['relation'], base, fields[base]['string'],
1213 info[base] = fields[base]['string']
1217 def graft_subfields(self, req, model, prefix, prefix_string, fields):
1218 export_fields = [field.split('/', 1)[1] for field in fields]
1220 (prefix + '/' + k, prefix_string + '/' + v)
1221 for k, v in self.fields_info(req, model, export_fields).iteritems())
1223 #noinspection PyPropertyDefinition
1225 def content_type(self):
1226 """ Provides the format's content type """
1227 raise NotImplementedError()
1229 def filename(self, base):
1230 """ Creates a valid filename for the format (with extension) from the
1231 provided base name (exension-less)
1233 raise NotImplementedError()
1235 def from_data(self, fields, rows):
1236 """ Conversion method from OpenERP's export data to whatever the
1237 current export class outputs
1239 :params list fields: a list of fields to export
1240 :params list rows: a list of records to export
1244 raise NotImplementedError()
1246 @openerpweb.httprequest
1247 def index(self, req, data, token):
1248 model, fields, ids, domain, import_compat = \
1249 operator.itemgetter('model', 'fields', 'ids', 'domain',
1251 simplejson.loads(data))
1253 context = req.session.eval_context(req.context)
1254 Model = req.session.model(model)
1255 ids = ids or Model.search(domain, context=context)
1257 field_names = map(operator.itemgetter('name'), fields)
1258 import_data = Model.export_data(ids, field_names, context).get('datas',[])
1261 columns_headers = field_names
1263 columns_headers = [val['label'].strip() for val in fields]
1266 return req.make_response(self.from_data(columns_headers, import_data),
1267 headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1268 ('Content-Type', self.content_type)],
1269 cookies={'fileToken': int(token)})
1271 class CSVExport(Export):
1272 _cp_path = '/web/export/csv'
1273 fmt = ('csv', 'CSV')
1276 def content_type(self):
1277 return 'text/csv;charset=utf8'
1279 def filename(self, base):
1280 return base + '.csv'
1282 def from_data(self, fields, rows):
1284 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1286 writer.writerow(fields)
1291 if isinstance(d, basestring):
1292 d = d.replace('\n',' ').replace('\t',' ')
1294 d = d.encode('utf-8')
1297 if d is False: d = None
1299 writer.writerow(row)
1306 class ExcelExport(Export):
1307 _cp_path = '/web/export/xls'
1308 fmt = ('xls', 'Excel')
1311 def content_type(self):
1312 return 'application/vnd.ms-excel'
1314 def filename(self, base):
1315 return base + '.xls'
1317 def from_data(self, fields, rows):
1320 workbook = xlwt.Workbook()
1321 worksheet = workbook.add_sheet('Sheet 1')
1323 for i, fieldname in enumerate(fields):
1324 worksheet.write(0, i, str(fieldname))
1325 worksheet.col(i).width = 8000 # around 220 pixels
1327 style = xlwt.easyxf('align: wrap yes')
1329 for row_index, row in enumerate(rows):
1330 for cell_index, cell_value in enumerate(row):
1331 if isinstance(cell_value, basestring):
1332 cell_value = re.sub("\r", " ", cell_value)
1333 worksheet.write(row_index + 1, cell_index, cell_value, style)