1 # -*- coding: utf-8 -*-
12 from xml.etree import ElementTree
13 from cStringIO import StringIO
19 import openerpweb.nonliterals
21 from babel.messages.pofile import read_po
23 # Should move to openerpweb.Xml2Json
26 # Simple and straightforward XML-to-JSON converter in Python
29 # URL: http://code.google.com/p/xml2json-direct/
31 def convert_to_json(s):
32 return simplejson.dumps(
33 Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
36 def convert_to_structure(s):
37 root = ElementTree.fromstring(s)
38 return Xml2Json.convert_element(root)
41 def convert_element(el, skip_whitespaces=True):
44 ns, name = el.tag.rsplit("}", 1)
46 res["namespace"] = ns[1:]
50 for k, v in el.items():
53 if el.text and (not skip_whitespaces or el.text.strip() != ''):
56 kids.append(Xml2Json.convert_element(kid))
57 if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
59 res["children"] = kids
62 #----------------------------------------------------------
63 # OpenERP Web base Controllers
64 #----------------------------------------------------------
66 def manifest_glob(addons, key):
69 globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
71 for pattern in globlist:
72 for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
73 files.append(path[len(openerpweb.path_addons):])
76 def concat_files(file_list):
77 """ Concatenate file content
78 return (concat,timestamp)
79 concat: concatenation of file content
80 timestamp: max(os.path.getmtime of file_list)
82 root = openerpweb.path_root
86 fname = os.path.join(root, i)
87 ftime = os.path.getmtime(fname)
88 if ftime > files_timestamp:
89 files_timestamp = ftime
90 files_content = open(fname).read()
91 files_concat = "".join(files_content)
94 home_template = textwrap.dedent("""<!DOCTYPE html>
95 <html style="height: 100%%">
97 <meta http-equiv="content-type" content="text/html; charset=utf-8" />
98 <title>OpenERP</title>
100 <script type="text/javascript">
102 QWeb = new QWeb2.Engine();
103 openerp.init().base.webclient("oe");
106 <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
109 <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
112 <body id="oe" class="openerp"></body>
115 class WebClient(openerpweb.Controller):
116 _cp_path = "/base/webclient"
118 @openerpweb.jsonrequest
119 def csslist(self, req, mods='base'):
120 return manifest_glob(mods.split(','), 'css')
122 @openerpweb.jsonrequest
123 def jslist(self, req, mods='base'):
124 return manifest_glob(mods.split(','), 'js')
126 @openerpweb.httprequest
127 def css(self, req, mods='base'):
128 cherrypy.response.headers['Content-Type'] = 'text/css'
129 files = manifest_glob(mods.split(','), 'css')
130 concat = concat_files(files)[0]
131 # TODO request set the Date of last modif and Etag
134 @openerpweb.httprequest
135 def js(self, req, mods='base'):
136 cherrypy.response.headers['Content-Type'] = 'application/javascript'
137 files = manifest_glob(mods.split(','), 'js')
138 concat = concat_files(files)[0]
139 # TODO request set the Date of last modif and Etag
142 @openerpweb.httprequest
143 def home(self, req, s_action=None):
145 jslist = ['/base/webclient/js']
147 jslist = manifest_glob(['base'], 'js')
148 js = "\n ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
151 csslist = ['/base/webclient/css']
153 csslist = manifest_glob(['base'], '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):
164 for addon_name in mods:
165 transl = {"messages":[]}
166 transs[addon_name] = transl
167 f_name = os.path.join(openerpweb.path_addons, addon_name, "po", lang + ".po")
168 if not os.path.exists(f_name):
171 with open(f_name) as t_file:
177 transl["messages"].append({'id': x.id, 'string': x.string})
178 return {"modules": transs}
181 class Database(openerpweb.Controller):
182 _cp_path = "/base/database"
184 @openerpweb.jsonrequest
185 def get_list(self, req):
186 proxy = req.session.proxy("db")
188 h = req.httprequest.headers['Host'].split(':')[0]
190 r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
191 dbs = [i for i in dbs if re.match(r, i)]
192 return {"db_list": dbs}
194 @openerpweb.jsonrequest
195 def progress(self, req, password, id):
196 return req.session.proxy('db').get_progress(password, id)
198 @openerpweb.jsonrequest
199 def create(self, req, fields):
201 params = dict(map(operator.itemgetter('name', 'value'), fields))
203 params['super_admin_pwd'],
205 bool(params.get('demo_data')),
207 params['create_admin_pwd']
211 return req.session.proxy("db").create(*create_attrs)
212 except xmlrpclib.Fault, e:
213 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
214 return {'error': e.faultCode, 'title': 'Create Database'}
215 return {'error': 'Could not create database !', 'title': 'Create Database'}
217 @openerpweb.jsonrequest
218 def drop(self, req, fields):
219 password, db = operator.itemgetter(
220 'drop_pwd', 'drop_db')(
221 dict(map(operator.itemgetter('name', 'value'), fields)))
224 return req.session.proxy("db").drop(password, db)
225 except xmlrpclib.Fault, e:
226 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
227 return {'error': e.faultCode, 'title': 'Drop Database'}
228 return {'error': 'Could not drop database !', 'title': 'Drop Database'}
230 @openerpweb.httprequest
231 def backup(self, req, backup_db, backup_pwd, token):
233 db_dump = base64.decodestring(
234 req.session.proxy("db").dump(backup_pwd, backup_db))
235 cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
236 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
237 cherrypy.response.cookie['fileToken'] = token
238 cherrypy.response.cookie['fileToken']['path'] = '/'
240 except xmlrpclib.Fault, e:
241 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
242 return 'Backup Database|' + e.faultCode
243 return 'Backup Database|Could not generate database backup'
245 @openerpweb.httprequest
246 def restore(self, req, db_file, restore_pwd, new_db):
248 data = base64.encodestring(db_file.file.read())
249 req.session.proxy("db").restore(restore_pwd, new_db, data)
251 except xmlrpclib.Fault, e:
252 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
253 raise cherrypy.HTTPError(403)
255 raise cherrypy.HTTPError()
257 @openerpweb.jsonrequest
258 def change_password(self, req, fields):
259 old_password, new_password = operator.itemgetter(
260 'old_pwd', 'new_pwd')(
261 dict(map(operator.itemgetter('name', 'value'), fields)))
263 return req.session.proxy("db").change_admin_password(old_password, new_password)
264 except xmlrpclib.Fault, e:
265 if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
266 return {'error': e.faultCode, 'title': 'Change Password'}
267 return {'error': 'Error, password not changed !', 'title': 'Change Password'}
269 class Session(openerpweb.Controller):
270 _cp_path = "/base/session"
272 @openerpweb.jsonrequest
273 def login(self, req, db, login, password):
274 req.session.login(db, login, password)
275 ctx = req.session.get_context()
278 "session_id": req.session_id,
279 "uid": req.session._uid,
283 @openerpweb.jsonrequest
284 def sc_list(self, req):
285 return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
286 req.session.eval_context(req.context))
288 @openerpweb.jsonrequest
289 def get_lang_list(self, req):
292 'lang_list': (req.session.proxy("db").list_lang() or []),
296 return {"error": e, "title": "Languages"}
298 @openerpweb.jsonrequest
299 def modules(self, req):
300 # TODO query server for installed web modules
302 for name, manifest in openerpweb.addons_manifest.items():
303 if name != 'base' and manifest.get('active', True):
307 @openerpweb.jsonrequest
308 def eval_domain_and_context(self, req, contexts, domains,
310 """ Evaluates sequences of domains and contexts, composing them into
311 a single context, domain or group_by sequence.
313 :param list contexts: list of contexts to merge together. Contexts are
314 evaluated in sequence, all previous contexts
315 are part of their own evaluation context
316 (starting at the session context).
317 :param list domains: list of domains to merge together. Domains are
318 evaluated in sequence and appended to one another
319 (implicit AND), their evaluation domain is the
320 result of merging all contexts.
321 :param list group_by_seq: list of domains (which may be in a different
322 order than the ``contexts`` parameter),
323 evaluated in sequence, their ``'group_by'``
324 key is extracted if they have one.
329 the global context created by merging all of
333 the concatenation of all domains
336 a list of fields to group by, potentially empty (in which case
337 no group by should be performed)
339 context, domain = eval_context_and_domain(req.session,
340 openerpweb.nonliterals.CompoundContext(*(contexts or [])),
341 openerpweb.nonliterals.CompoundDomain(*(domains or [])))
343 group_by_sequence = []
344 for candidate in (group_by_seq or []):
345 ctx = req.session.eval_context(candidate, context)
346 group_by = ctx.get('group_by')
349 elif isinstance(group_by, basestring):
350 group_by_sequence.append(group_by)
352 group_by_sequence.extend(group_by)
357 'group_by': group_by_sequence
360 @openerpweb.jsonrequest
361 def save_session_action(self, req, the_action):
363 This method store an action object in the session object and returns an integer
364 identifying that action. The method get_session_action() can be used to get
367 :param the_action: The action to save in the session.
368 :type the_action: anything
369 :return: A key identifying the saved action.
372 saved_actions = cherrypy.session.get('saved_actions')
373 if not saved_actions:
374 saved_actions = {"next":0, "actions":{}}
375 cherrypy.session['saved_actions'] = saved_actions
376 # we don't allow more than 10 stored actions
377 if len(saved_actions["actions"]) >= 10:
378 del saved_actions["actions"][min(saved_actions["actions"].keys())]
379 key = saved_actions["next"]
380 saved_actions["actions"][key] = the_action
381 saved_actions["next"] = key + 1
384 @openerpweb.jsonrequest
385 def get_session_action(self, req, key):
387 Gets back a previously saved action. This method can return None if the action
388 was saved since too much time (this case should be handled in a smart way).
390 :param key: The key given by save_session_action()
392 :return: The saved action or None.
395 saved_actions = cherrypy.session.get('saved_actions')
396 if not saved_actions:
398 return saved_actions["actions"].get(key)
400 @openerpweb.jsonrequest
401 def check(self, req):
402 req.session.assert_valid()
405 def eval_context_and_domain(session, context, domain=None):
406 e_context = session.eval_context(context)
407 # should we give the evaluated context as an evaluation context to the domain?
408 e_domain = session.eval_domain(domain or [])
410 return e_context, e_domain
412 def load_actions_from_ir_values(req, key, key2, models, meta, context):
413 Values = req.session.model('ir.values')
414 actions = Values.get(key, key2, models, meta, context)
416 return [(id, name, clean_action(action, req.session, context=context))
417 for id, name, action in actions]
419 def clean_action(action, session, context=None):
420 action.setdefault('flags', {})
421 if action['type'] != 'ir.actions.act_window':
423 # values come from the server, we can just eval them
424 if isinstance(action.get('context'), basestring):
425 action['context'] = eval(
427 session.evaluation_context(context=context)) or {}
429 if isinstance(action.get('domain'), basestring):
430 action['domain'] = eval(
432 session.evaluation_context(
433 action.get('context', {}))) or []
435 return fix_view_modes(action)
437 def generate_views(action):
439 While the server generates a sequence called "views" computing dependencies
440 between a bunch of stuff for views coming directly from the database
441 (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
442 to return custom view dictionaries generated on the fly.
444 In that case, there is no ``views`` key available on the action.
446 Since the web client relies on ``action['views']``, generate it here from
447 ``view_mode`` and ``view_id``.
449 Currently handles two different cases:
451 * no view_id, multiple view_mode
452 * single view_id, single view_mode
454 :param dict action: action descriptor dictionary to generate a views key for
456 view_id = action.get('view_id', False)
457 if isinstance(view_id, (list, tuple)):
460 # providing at least one view mode is a requirement, not an option
461 view_modes = action['view_mode'].split(',')
463 if len(view_modes) > 1:
465 raise ValueError('Non-db action dictionaries should provide '
466 'either multiple view modes or a single view '
467 'mode and an optional view id.\n\n Got view '
468 'modes %r and view id %r for action %r' % (
469 view_modes, view_id, action))
470 action['views'] = [(False, mode) for mode in view_modes]
472 action['views'] = [(view_id, view_modes[0])]
474 def fix_view_modes(action):
475 """ For historical reasons, OpenERP has weird dealings in relation to
476 view_mode and the view_type attribute (on window actions):
478 * one of the view modes is ``tree``, which stands for both list views
480 * the choice is made by checking ``view_type``, which is either
481 ``form`` for a list view or ``tree`` for an actual tree view
483 This methods simply folds the view_type into view_mode by adding a
484 new view mode ``list`` which is the result of the ``tree`` view_mode
485 in conjunction with the ``form`` view_type.
487 TODO: this should go into the doc, some kind of "peculiarities" section
489 :param dict action: an action descriptor
490 :returns: nothing, the action is modified in place
492 if 'views' not in action:
493 generate_views(action)
495 if action.pop('view_type') != 'form':
499 [id, mode if mode != 'tree' else 'list']
500 for id, mode in action['views']
505 class Menu(openerpweb.Controller):
506 _cp_path = "/base/menu"
508 @openerpweb.jsonrequest
510 return {'data': self.do_load(req)}
512 def do_load(self, req):
513 """ Loads all menu items (all applications and their sub-menus).
515 :param req: A request object, with an OpenERP session attribute
516 :type req: < session -> OpenERPSession >
517 :return: the menu root
518 :rtype: dict('children': menu_nodes)
520 Menus = req.session.model('ir.ui.menu')
521 # menus are loaded fully unlike a regular tree view, cause there are
522 # less than 512 items
523 context = req.session.eval_context(req.context)
524 menu_ids = Menus.search([], 0, False, False, context)
525 menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
526 menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
527 menu_items.append(menu_root)
529 # make a tree using parent_id
530 menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
531 for menu_item in menu_items:
532 if menu_item['parent_id']:
533 parent = menu_item['parent_id'][0]
536 if parent in menu_items_map:
537 menu_items_map[parent].setdefault(
538 'children', []).append(menu_item)
540 # sort by sequence a tree using parent_id
541 for menu_item in menu_items:
542 menu_item.setdefault('children', []).sort(
543 key=lambda x:x["sequence"])
547 @openerpweb.jsonrequest
548 def action(self, req, menu_id):
549 actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
550 [('ir.ui.menu', menu_id)], False,
551 req.session.eval_context(req.context))
552 return {"action": actions}
554 class DataSet(openerpweb.Controller):
555 _cp_path = "/base/dataset"
557 @openerpweb.jsonrequest
558 def fields(self, req, model):
559 return {'fields': req.session.model(model).fields_get(False,
560 req.session.eval_context(req.context))}
562 @openerpweb.jsonrequest
563 def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
564 return self.do_search_read(request, model, fields, offset, limit, domain, sort)
565 def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
567 """ Performs a search() followed by a read() (if needed) using the
568 provided search criteria
570 :param request: a JSON-RPC request object
571 :type request: openerpweb.JsonRequest
572 :param str model: the name of the model to search on
573 :param fields: a list of the fields to return in the result records
575 :param int offset: from which index should the results start being returned
576 :param int limit: the maximum number of records to return
577 :param list domain: the search domain for the query
578 :param list sort: sorting directives
579 :returns: A structure (dict) with two keys: ids (all the ids matching
580 the (domain, context) pair) and records (paginated records
581 matching fields selection set)
584 Model = request.session.model(model)
586 context, domain = eval_context_and_domain(
587 request.session, request.context, domain)
589 ids = Model.search(domain, 0, False, sort or False, context)
590 # need to fill the dataset with all ids for the (domain, context) pair,
591 # so search un-paginated and paginate manually before reading
592 paginated_ids = ids[offset:(offset + limit if limit else None)]
593 if fields and fields == ['id']:
594 # shortcut read if we only want the ids
597 'records': map(lambda id: {'id': id}, paginated_ids)
600 records = Model.read(paginated_ids, fields or False, context)
601 records.sort(key=lambda obj: ids.index(obj['id']))
608 @openerpweb.jsonrequest
609 def read(self, request, model, ids, fields=False):
610 return self.do_search_read(request, model, ids, fields)
612 @openerpweb.jsonrequest
613 def get(self, request, model, ids, fields=False):
614 return self.do_get(request, model, ids, fields)
616 def do_get(self, request, model, ids, fields=False):
617 """ Fetches and returns the records of the model ``model`` whose ids
620 The results are in the same order as the inputs, but elements may be
621 missing (if there is no record left for the id)
623 :param request: the JSON-RPC2 request object
624 :type request: openerpweb.JsonRequest
625 :param model: the model to read from
627 :param ids: a list of identifiers
629 :param fields: a list of fields to fetch, ``False`` or empty to fetch
630 all fields in the model
631 :type fields: list | False
632 :returns: a list of records, in the same order as the list of ids
635 Model = request.session.model(model)
636 records = Model.read(ids, fields, request.session.eval_context(request.context))
638 record_map = dict((record['id'], record) for record in records)
640 return [record_map[id] for id in ids if record_map.get(id)]
642 @openerpweb.jsonrequest
643 def load(self, req, model, id, fields):
644 m = req.session.model(model)
646 r = m.read([id], False, req.session.eval_context(req.context))
649 return {'value': value}
651 @openerpweb.jsonrequest
652 def create(self, req, model, data):
653 m = req.session.model(model)
654 r = m.create(data, req.session.eval_context(req.context))
657 @openerpweb.jsonrequest
658 def save(self, req, model, id, data):
659 m = req.session.model(model)
660 r = m.write([id], data, req.session.eval_context(req.context))
663 @openerpweb.jsonrequest
664 def unlink(self, request, model, ids=()):
665 Model = request.session.model(model)
666 return Model.unlink(ids, request.session.eval_context(request.context))
668 def call_common(self, req, model, method, args, domain_id=None, context_id=None):
669 domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id else []
670 context = args[context_id] if context_id and len(args) - 1 >= context_id else {}
671 c, d = eval_context_and_domain(req.session, context, domain)
672 if domain_id and len(args) - 1 >= domain_id:
674 if context_id and len(args) - 1 >= context_id:
677 return getattr(req.session.model(model), method)(*args)
679 @openerpweb.jsonrequest
680 def call(self, req, model, method, args, domain_id=None, context_id=None):
681 return self.call_common(req, model, method, args, domain_id, context_id)
683 @openerpweb.jsonrequest
684 def call_button(self, req, model, method, args, domain_id=None, context_id=None):
685 action = self.call_common(req, model, method, args, domain_id, context_id)
686 if isinstance(action, dict) and action.get('type') != '':
687 return {'result': clean_action(action, req.session)}
688 return {'result': False}
690 @openerpweb.jsonrequest
691 def exec_workflow(self, req, model, id, signal):
692 r = req.session.exec_workflow(model, id, signal)
695 @openerpweb.jsonrequest
696 def default_get(self, req, model, fields):
697 Model = req.session.model(model)
698 return Model.default_get(fields, req.session.eval_context(req.context))
700 @openerpweb.jsonrequest
701 def name_search(self, req, model, search_str, domain=[], context={}):
702 m = req.session.model(model)
703 r = m.name_search(search_str+'%', domain, '=ilike', context)
706 class DataGroup(openerpweb.Controller):
707 _cp_path = "/base/group"
708 @openerpweb.jsonrequest
709 def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
710 Model = request.session.model(model)
711 context, domain = eval_context_and_domain(request.session, request.context, domain)
713 return Model.read_group(
714 domain or [], fields, group_by_fields, 0, False,
715 dict(context, group_by=group_by_fields), sort or False)
717 class View(openerpweb.Controller):
718 _cp_path = "/base/view"
720 def fields_view_get(self, request, model, view_id, view_type,
721 transform=True, toolbar=False, submenu=False):
722 Model = request.session.model(model)
723 context = request.session.eval_context(request.context)
724 fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
725 # todo fme?: check that we should pass the evaluated context here
726 self.process_view(request.session, fvg, context, transform)
729 def process_view(self, session, fvg, context, transform):
730 # depending on how it feels, xmlrpclib.ServerProxy can translate
731 # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
732 # enjoy unicode strings which can not be trivially converted to
733 # strings, and it blows up during parsing.
735 # So ensure we fix this retardation by converting view xml back to
737 if isinstance(fvg['arch'], unicode):
738 arch = fvg['arch'].encode('utf-8')
743 evaluation_context = session.evaluation_context(context or {})
744 xml = self.transform_view(arch, session, evaluation_context)
746 xml = ElementTree.fromstring(arch)
747 fvg['arch'] = Xml2Json.convert_element(xml)
749 for field in fvg['fields'].itervalues():
750 if field.get('views'):
751 for view in field["views"].itervalues():
752 self.process_view(session, view, None, transform)
753 if field.get('domain'):
754 field["domain"] = self.parse_domain(field["domain"], session)
755 if field.get('context'):
756 field["context"] = self.parse_context(field["context"], session)
758 @openerpweb.jsonrequest
759 def add_custom(self, request, view_id, arch):
760 CustomView = request.session.model('ir.ui.view.custom')
762 'user_id': request.session._uid,
765 }, request.session.eval_context(request.context))
766 return {'result': True}
768 @openerpweb.jsonrequest
769 def undo_custom(self, request, view_id, reset=False):
770 CustomView = request.session.model('ir.ui.view.custom')
771 context = request.session.eval_context(request.context)
772 vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
773 0, False, False, context)
776 CustomView.unlink(vcustom, context)
778 CustomView.unlink([vcustom[0]], context)
779 return {'result': True}
780 return {'result': False}
782 def transform_view(self, view_string, session, context=None):
783 # transform nodes on the fly via iterparse, instead of
784 # doing it statically on the parsing result
785 parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
787 for event, elem in parser:
791 self.parse_domains_and_contexts(elem, session)
794 def parse_domain(self, domain, session):
795 """ Parses an arbitrary string containing a domain, transforms it
796 to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
798 :param domain: the domain to parse, if the domain is not a string it
799 is assumed to be a literal domain and is returned as-is
800 :param session: Current OpenERP session
801 :type session: openerpweb.openerpweb.OpenERPSession
803 if not isinstance(domain, (str, unicode)):
806 return openerpweb.ast.literal_eval(domain)
809 return openerpweb.nonliterals.Domain(session, domain)
811 def parse_context(self, context, session):
812 """ Parses an arbitrary string containing a context, transforms it
813 to either a literal context or a :class:`openerpweb.nonliterals.Context`
815 :param context: the context to parse, if the context is not a string it
816 is assumed to be a literal domain and is returned as-is
817 :param session: Current OpenERP session
818 :type session: openerpweb.openerpweb.OpenERPSession
820 if not isinstance(context, (str, unicode)):
823 return openerpweb.ast.literal_eval(context)
825 return openerpweb.nonliterals.Context(session, context)
827 def parse_domains_and_contexts(self, elem, session):
828 """ Converts domains and contexts from the view into Python objects,
829 either literals if they can be parsed by literal_eval or a special
830 placeholder object if the domain or context refers to free variables.
832 :param elem: the current node being parsed
833 :type param: xml.etree.ElementTree.Element
834 :param session: OpenERP session object, used to store and retrieve
836 :type session: openerpweb.openerpweb.OpenERPSession
838 for el in ['domain', 'filter_domain']:
839 domain = elem.get(el, '').strip()
841 elem.set(el, self.parse_domain(domain, session))
842 for el in ['context', 'default_get']:
843 context_string = elem.get(el, '').strip()
845 elem.set(el, self.parse_context(context_string, session))
847 class FormView(View):
848 _cp_path = "/base/formview"
850 @openerpweb.jsonrequest
851 def load(self, req, model, view_id, toolbar=False):
852 fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
853 return {'fields_view': fields_view}
855 class ListView(View):
856 _cp_path = "/base/listview"
858 @openerpweb.jsonrequest
859 def load(self, req, model, view_id, toolbar=False):
860 fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
861 return {'fields_view': fields_view}
863 def process_colors(self, view, row, context):
864 colors = view['arch']['attrs'].get('colors')
871 for pair in colors.split(';')
872 if eval(pair.split(':')[1], dict(context, **row))
877 elif len(color) == 1:
881 class SearchView(View):
882 _cp_path = "/base/searchview"
884 @openerpweb.jsonrequest
885 def load(self, req, model, view_id):
886 fields_view = self.fields_view_get(req, model, view_id, 'search')
887 return {'fields_view': fields_view}
889 @openerpweb.jsonrequest
890 def fields_get(self, req, model):
891 Model = req.session.model(model)
892 fields = Model.fields_get(False, req.session.eval_context(req.context))
893 for field in fields.values():
894 # shouldn't convert the views too?
895 if field.get('domain'):
896 field["domain"] = self.parse_domain(field["domain"], req.session)
897 if field.get('context'):
898 field["context"] = self.parse_domain(field["context"], req.session)
899 return {'fields': fields}
901 @openerpweb.jsonrequest
902 def get_filters(self, req, model):
903 Model = req.session.model("ir.filters")
904 filters = Model.get_filters(model)
905 for filter in filters:
906 filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
907 filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
910 @openerpweb.jsonrequest
911 def save_filter(self, req, model, name, context_to_save, domain):
912 Model = req.session.model("ir.filters")
913 ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
914 ctx.session = req.session
916 domain = openerpweb.nonliterals.CompoundDomain(domain)
917 domain.session = req.session
918 domain = domain.evaluate()
919 uid = req.session._uid
920 context = req.session.eval_context(req.context)
921 to_return = Model.create_or_replace({"context": ctx,
929 class Binary(openerpweb.Controller):
930 _cp_path = "/base/binary"
932 @openerpweb.httprequest
933 def image(self, request, session_id, model, id, field, **kw):
934 cherrypy.response.headers['Content-Type'] = 'image/png'
935 Model = request.session.model(model)
936 context = request.session.eval_context(request.context)
939 res = Model.default_get([field], context).get(field, '')
941 res = Model.read([int(id)], [field], context)[0].get(field, '')
942 return base64.decodestring(res)
943 except: # TODO: what's the exception here?
944 return self.placeholder()
945 def placeholder(self):
946 return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
948 @openerpweb.httprequest
949 def saveas(self, request, session_id, model, id, field, fieldname, **kw):
950 Model = request.session.model(model)
951 context = request.session.eval_context(request.context)
952 res = Model.read([int(id)], [field, fieldname], context)[0]
953 filecontent = res.get(field, '')
955 raise cherrypy.NotFound
957 cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
958 filename = '%s_%s' % (model.replace('.', '_'), id)
960 filename = res.get(fieldname, '') or filename
961 cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' + filename
962 return base64.decodestring(filecontent)
964 @openerpweb.httprequest
965 def upload(self, request, session_id, callback, ufile=None):
966 cherrypy.response.timeout = 500
968 for key, val in cherrypy.request.headers.iteritems():
969 headers[key.lower()] = val
970 size = int(headers.get('content-length', 0))
971 # TODO: might be useful to have a configuration flag for max-length file uploads
973 out = """<script language="javascript" type="text/javascript">
974 var win = window.top.window,
976 if (typeof(callback) === 'function') {
977 callback.apply(this, %s);
979 win.jQuery('#oe_notification', win.document).notify('create', {
980 title: "Ajax File Upload",
981 text: "Could not find callback"
985 data = ufile.file.read()
986 args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
988 args = [False, e.message]
989 return out % (simplejson.dumps(callback), simplejson.dumps(args))
991 @openerpweb.httprequest
992 def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
993 cherrypy.response.timeout = 500
994 context = request.session.eval_context(request.context)
995 Model = request.session.model('ir.attachment')
997 out = """<script language="javascript" type="text/javascript">
998 var win = window.top.window,
1000 if (typeof(callback) === 'function') {
1001 callback.call(this, %s);
1004 attachment_id = Model.create({
1005 'name': ufile.filename,
1006 'datas': base64.encodestring(ufile.file.read()),
1011 'filename': ufile.filename,
1014 except Exception, e:
1015 args = { 'error': e.message }
1016 return out % (simplejson.dumps(callback), simplejson.dumps(args))
1018 class Action(openerpweb.Controller):
1019 _cp_path = "/base/action"
1021 @openerpweb.jsonrequest
1022 def load(self, req, action_id):
1023 Actions = req.session.model('ir.actions.actions')
1025 context = req.session.eval_context(req.context)
1026 action_type = Actions.read([action_id], ['type'], context)
1028 action = req.session.model(action_type[0]['type']).read([action_id], False,
1031 value = clean_action(action[0], req.session)
1032 return {'result': value}
1034 @openerpweb.jsonrequest
1035 def run(self, req, action_id):
1036 return clean_action(req.session.model('ir.actions.server').run(
1037 [action_id], req.session.eval_context(req.context)), req.session)
1039 class TreeView(View):
1040 _cp_path = "/base/treeview"
1042 @openerpweb.jsonrequest
1043 def load(self, req, model, view_id, toolbar=False):
1044 return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1046 @openerpweb.jsonrequest
1047 def action(self, req, model, id):
1048 return load_actions_from_ir_values(
1049 req,'action', 'tree_but_open',[(model, id)],
1050 False, req.session.eval_context(req.context))
1052 def export_csv(fields, result):
1054 writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1056 writer.writerow(fields)
1061 if isinstance(d, basestring):
1062 d = d.replace('\n',' ').replace('\t',' ')
1064 d = d.encode('utf-8')
1067 if d is False: d = None
1069 writer.writerow(row)
1076 def export_xls(fieldnames, table):
1080 common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1082 workbook = xlwt.Workbook()
1083 worksheet = workbook.add_sheet('Sheet 1')
1085 for i, fieldname in enumerate(fieldnames):
1086 worksheet.write(0, i, str(fieldname))
1087 worksheet.col(i).width = 8000 # around 220 pixels
1089 style = xlwt.easyxf('align: wrap yes')
1091 for row_index, row in enumerate(table):
1092 for cell_index, cell_value in enumerate(row):
1093 cell_value = str(cell_value)
1094 cell_value = re.sub("\r", " ", cell_value)
1095 worksheet.write(row_index + 1, cell_index, cell_value, style)
1103 #return data.decode('ISO-8859-1')
1104 return unicode(data, 'utf-8', 'replace')
1107 _cp_path = "/base/export"
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='', name= '', field_parent=None, params={}):
1116 import_compat = params.get("import_compat", False)
1118 fields = self.fields_get(req, model)
1119 field_parent_type = params.get("parent_field_type",False)
1121 if import_compat and field_parent_type and field_parent_type == "many2one":
1124 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1126 fields_order = fields.keys()
1127 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1129 for index, field in enumerate(fields_order):
1130 value = fields[field]
1132 if import_compat and value.get('readonly', False):
1134 for sl in value.get('states', {}).values():
1136 ok = ok or (s==['readonly',False])
1139 id = prefix + (prefix and '/'or '') + field
1140 nm = name + (name and '/' or '') + value['string']
1141 record.update(id=id, string= nm, action='javascript: void(0)',
1142 target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1143 records.append(record)
1145 if len(nm.split('/')) < 3 and value.get('relation', False):
1147 ref = value.pop('relation')
1148 cfields = self.fields_get(req, ref)
1149 if (value['type'] == 'many2many'):
1150 record['children'] = []
1151 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1153 elif value['type'] == 'many2one':
1154 record['children'] = [id + '/id', id + '/.id']
1155 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1158 cfields_order = cfields.keys()
1159 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1161 for j, fld in enumerate(cfields_order):
1162 cid = id + '/' + fld
1163 cid = cid.replace(' ', '_')
1164 children.append(cid)
1165 record['children'] = children or []
1166 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1168 ref = value.pop('relation')
1169 cfields = self.fields_get(req, ref)
1170 cfields_order = cfields.keys()
1171 cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1173 for j, fld in enumerate(cfields_order):
1174 cid = id + '/' + fld
1175 cid = cid.replace(' ', '_')
1176 children.append(cid)
1177 record['children'] = children or []
1178 record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1183 @openerpweb.jsonrequest
1184 def save_export_lists(self, req, name, model, field_list):
1185 result = {'resource':model, 'name':name, 'export_fields': []}
1186 for field in field_list:
1187 result['export_fields'].append((0, 0, {'name': field}))
1188 return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1190 @openerpweb.jsonrequest
1191 def exist_export_lists(self, req, model):
1192 export_model = req.session.model("ir.exports")
1193 return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1195 @openerpweb.jsonrequest
1196 def delete_export(self, req, export_id):
1197 req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1200 @openerpweb.jsonrequest
1201 def namelist(self,req, model, export_id):
1203 result = self.get_data(req, model, req.session.eval_context(req.context))
1204 ir_export_obj = req.session.model("ir.exports")
1205 ir_export_line_obj = req.session.model("ir.exports.line")
1207 field = ir_export_obj.read(export_id)
1208 fields = ir_export_line_obj.read(field['export_fields'])
1211 [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1214 def get_data(self, req, model, context=None):
1216 context = context or {}
1218 proxy = req.session.model(model)
1219 fields = self.fields_get(req, model)
1221 f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1222 f2 = proxy.fields_view_get(False, 'form', context)['fields']
1226 fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1229 _fields = {'id': 'ID' , '.id': 'Database ID' }
1230 def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1231 fields_order = fields.keys()
1232 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1234 for field in fields_order:
1235 fields_data[prefix_node+field] = fields[field]
1237 fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1238 st_name = fields[field]['string'] or field
1239 _fields[prefix_node+field] = st_name
1240 if fields[field].get('relation', False) and level>0:
1241 fields2 = self.fields_get(req, fields[field]['relation'])
1242 fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1243 model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1244 model_populate(fields)
1248 @openerpweb.jsonrequest
1249 def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1250 context = req.session.eval_context(req.context)
1251 modle_obj = req.session.model(model)
1252 ids = ids or modle_obj.search(domain, context=context)
1254 field = fields.keys()
1255 result = modle_obj.export_data(ids, field , context).get('datas',[])
1257 if not import_compat:
1258 field = [val.strip() for val in fields.values()]
1260 if export_format == 'xls':
1261 return export_xls(field, result)
1263 return export_csv(field, result)