[FIX] get correct field name in chane_password method
[odoo/odoo.git] / addons / base / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import base64, glob, os, re
4 from xml.etree import ElementTree
5 from cStringIO import StringIO
6
7 import operator
8 import simplejson
9
10 import openerpweb
11 import openerpweb.ast
12 import openerpweb.nonliterals
13
14 import cherrypy
15 import xmlrpclib
16
17 # Should move to openerpweb.Xml2Json
18 class Xml2Json:
19     # xml2json-direct
20     # Simple and straightforward XML-to-JSON converter in Python
21     # New BSD Licensed
22     #
23     # URL: http://code.google.com/p/xml2json-direct/
24     @staticmethod
25     def convert_to_json(s):
26         return simplejson.dumps(
27             Xml2Json.convert_to_structure(s), sort_keys=True, indent=4)
28
29     @staticmethod
30     def convert_to_structure(s):
31         root = ElementTree.fromstring(s)
32         return Xml2Json.convert_element(root)
33
34     @staticmethod
35     def convert_element(el, skip_whitespaces=True):
36         res = {}
37         if el.tag[0] == "{":
38             ns, name = el.tag.rsplit("}", 1)
39             res["tag"] = name
40             res["namespace"] = ns[1:]
41         else:
42             res["tag"] = el.tag
43         res["attrs"] = {}
44         for k, v in el.items():
45             res["attrs"][k] = v
46         kids = []
47         if el.text and (not skip_whitespaces or el.text.strip() != ''):
48             kids.append(el.text)
49         for kid in el:
50             kids.append(Xml2Json.convert_element(kid))
51             if kid.tail and (not skip_whitespaces or kid.tail.strip() != ''):
52                 kids.append(kid.tail)
53         res["children"] = kids
54         return res
55
56 #----------------------------------------------------------
57 # OpenERP Web base Controllers
58 #----------------------------------------------------------
59
60 def manifest_glob(addons, key):
61     files = []
62     for addon in addons:
63         globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
64         print globlist
65         for pattern in globlist:
66             for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
67                 files.append(path[len(openerpweb.path_addons):])
68     return files
69
70 def concat_files(file_list):
71     """ Concatenate file content
72     return (concat,timestamp)
73     concat: concatenation of file content
74     timestamp: max(os.path.getmtime of file_list)
75     """
76     root = openerpweb.path_root
77     files_content = []
78     files_timestamp = 0
79     for i in file_list:
80         fname = os.path.join(root, i)
81         ftime = os.path.getmtime(fname)
82         if ftime > files_timestamp:
83             files_timestamp = ftime
84         files_content = open(fname).read()
85     files_concat = "".join(files_content)
86     return files_concat
87
88 class WebClient(openerpweb.Controller):
89     _cp_path = "/base/webclient"
90
91     @openerpweb.jsonrequest
92     def csslist(self, req, mods='base'):
93         return manifest_glob(mods.split(','), 'css')
94
95     @openerpweb.jsonrequest
96     def jslist(self, req, mods='base'):
97         return manifest_glob(mods.split(','), 'js')
98
99     @openerpweb.httprequest
100     def css(self, req, mods='base'):
101         cherrypy.response.headers['Content-Type'] = 'text/css'
102         files = manifest_glob(mods.split(','), 'css')
103         concat = concat_files(files)[0]
104         # TODO request set the Date of last modif and Etag
105         return concat
106
107     @openerpweb.httprequest
108     def js(self, req, mods='base'):
109         cherrypy.response.headers['Content-Type'] = 'application/javascript'
110         files = manifest_glob(mods.split(','), 'js')
111         concat = concat_files(files)[0]
112         # TODO request set the Date of last modif and Etag
113         return concat
114
115     @openerpweb.httprequest
116     def home(self, req):
117         template ="""<!DOCTYPE html>
118         <html style="height: 100%%">
119         <head>
120             <meta http-equiv="content-type" content="text/html; charset=utf-8" />
121             <title>OpenERP</title>
122             %s
123             <script type="text/javascript">
124             $(function() {
125                 QWeb = new QWeb2.Engine(); 
126                 openerp.init().base.webclient("oe"); 
127             });
128             </script>
129             <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
130             %s
131             <!--[if lte IE 7]>
132             <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
133             <![endif]-->
134         </head>
135         <body id="oe" class="openerp"></body>
136         </html>
137         """.replace('\n'+' '*8,'\n')
138
139         # script tags
140         jslist = ['/base/webclient/js']
141         if 1: # debug == 1
142             jslist = manifest_glob(['base'], 'js')
143         js = "\n    ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
144
145         # css tags
146         csslist = ['/base/webclient/css']
147         if 1: # debug == 1
148             csslist = manifest_glob(['base'], 'css')
149         css = "\n    ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
150         r = template % (js, css)
151         return r
152
153 class Database(openerpweb.Controller):
154     _cp_path = "/base/database"
155
156     @openerpweb.jsonrequest
157     def get_databases_list(self, req):
158         proxy = req.session.proxy("db")
159         dbs = proxy.list()
160         h = req.httprequest.headers['Host'].split(':')[0]
161         d = h.split('.')[0]
162         r = cherrypy.config['openerp.dbfilter'].replace('%h', h).replace('%d', d)
163         dbs = [i for i in dbs if re.match(r, i)]
164         return {"db_list": dbs}
165     
166     @openerpweb.jsonrequest
167     def create_db(self, req, fields):
168
169         params = dict(map(operator.itemgetter('name', 'value'), fields))
170         create_attrs = operator.itemgetter(
171             'super_admin_pwd', 'db_name', 'demo_data', 'db_lang', 'create_admin_pwd')(
172             params)
173
174         try:
175             return req.session.proxy("db").create(*create_attrs)
176         except xmlrpclib.Fault, e:
177             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
178                 return {'error': e.faultCode, 'title': 'Create Database'}
179         return {'error': 'Could not create database !', 'title': 'Create Database'}
180
181     @openerpweb.jsonrequest
182     def drop_db(self, req, fields):
183         password, db = operator.itemgetter(
184             'drop_pwd', 'drop_db')(
185                 dict(map(operator.itemgetter('name', 'value'), fields)))
186         
187         try:
188             return req.session.proxy("db").drop(password, db)
189         except xmlrpclib.Fault, e:
190             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
191                 return {'error': e.faultCode, 'title': 'Drop Database'}
192         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
193
194     @openerpweb.httprequest
195     def backup_db(self, req, backup_db, backup_pwd, token):
196         try:
197             db_dump = base64.decodestring(
198                 req.session.proxy("db").dump(backup_pwd, backup_db))
199             cherrypy.response.headers['Content-Type'] = "application/octet-stream; charset=binary"
200             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename="' + backup_db + '.dump"'
201             cherrypy.response.cookie['fileToken'] = token
202             cherrypy.response.cookie['fileToken']['path'] = '/'
203             return db_dump
204         except xmlrpclib.Fault, e:
205             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
206                 return {'error': e.faultCode, 'title': 'Backup Database'}
207         return {'error': 'Could not drop database !', 'title': 'Backup Database'}
208             
209     @openerpweb.httprequest
210     def restore_db(self, req, db_file, restore_pwd, new_db):
211         response = None
212         try:
213             data = base64.encodestring(db_file.file.read())
214             response = simplejson.dumps(
215                 req.session.proxy("db").restore(restore_pwd, new_db, data))
216         except xmlrpclib.Fault, e:
217             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
218                 response = simplejson.dumps({'error': e.faultCode, 'title': 'Restore Database'})
219         if not response:
220             response = simplejson.dumps({'error': 'Could not restore database !', 'title': 'Restore Database'})
221
222         cherrypy.response.headers['Content-Type'] = 'application/json'
223         cherrypy.response.headers['Content-Length'] = len(response)
224         return response
225
226     @openerpweb.jsonrequest
227     def change_password_db(self, req, fields):
228         old_password, new_password = operator.itemgetter(
229             'old_pwd', 'new_pwd')(
230                 dict(map(operator.itemgetter('name', 'value'), fields)))
231         try:
232             return req.session.proxy("db").change_admin_password(old_password, new_password)
233         except xmlrpclib.Fault, e:
234             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
235                 return {'error': e.faultCode, 'title': 'Change Password'}
236         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
237
238 class Session(openerpweb.Controller):
239     _cp_path = "/base/session"
240
241     @openerpweb.jsonrequest
242     def login(self, req, db, login, password):
243         req.session.login(db, login, password)
244
245         return {
246             "session_id": req.session_id,
247             "uid": req.session._uid,
248         }
249
250     @openerpweb.jsonrequest
251     def sc_list(self, req):
252         return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
253                                                          req.session.eval_context(req.context))
254
255     @openerpweb.jsonrequest
256     def get_lang_list(self, req):
257         try:
258             return {
259                 'lang_list': (req.session.proxy("db").list_lang() or []),
260                 'error': ""
261             }
262         except Exception, e:
263             return {"error": e, "title": "Languages"}
264             
265     @openerpweb.jsonrequest
266     def modules(self, req):
267         # TODO query server for installed web modules
268         mods = []
269         for name, manifest in openerpweb.addons_manifest.items():
270             if name != 'base' and manifest.get('active', True):
271                 mods.append(name)
272         return mods
273
274     @openerpweb.jsonrequest
275     def eval_domain_and_context(self, req, contexts, domains,
276                                 group_by_seq=None):
277         """ Evaluates sequences of domains and contexts, composing them into
278         a single context, domain or group_by sequence.
279
280         :param list contexts: list of contexts to merge together. Contexts are
281                               evaluated in sequence, all previous contexts
282                               are part of their own evaluation context
283                               (starting at the session context).
284         :param list domains: list of domains to merge together. Domains are
285                              evaluated in sequence and appended to one another
286                              (implicit AND), their evaluation domain is the
287                              result of merging all contexts.
288         :param list group_by_seq: list of domains (which may be in a different
289                                   order than the ``contexts`` parameter),
290                                   evaluated in sequence, their ``'group_by'``
291                                   key is extracted if they have one.
292         :returns:
293             a 3-dict of:
294
295             context (``dict``)
296                 the global context created by merging all of
297                 ``contexts``
298
299             domain (``list``)
300                 the concatenation of all domains
301
302             group_by (``list``)
303                 a list of fields to group by, potentially empty (in which case
304                 no group by should be performed)
305         """
306         context, domain = eval_context_and_domain(req.session,
307                                                   openerpweb.nonliterals.CompoundContext(*(contexts or [])),
308                                                   openerpweb.nonliterals.CompoundDomain(*(domains or [])))
309         
310         group_by_sequence = []
311         for candidate in (group_by_seq or []):
312             ctx = req.session.eval_context(candidate, context)
313             group_by = ctx.get('group_by')
314             if not group_by:
315                 continue
316             elif isinstance(group_by, basestring):
317                 group_by_sequence.append(group_by)
318             else:
319                 group_by_sequence.extend(group_by)
320         
321         return {
322             'context': context,
323             'domain': domain,
324             'group_by': group_by_sequence
325         }
326
327     @openerpweb.jsonrequest
328     def save_session_action(self, req, the_action):
329         """
330         This method store an action object in the session object and returns an integer
331         identifying that action. The method get_session_action() can be used to get
332         back the action.
333         
334         :param the_action: The action to save in the session.
335         :type the_action: anything
336         :return: A key identifying the saved action.
337         :rtype: integer
338         """
339         saved_actions = cherrypy.session.get('saved_actions')
340         if not saved_actions:
341             saved_actions = {"next":0, "actions":{}}
342             cherrypy.session['saved_actions'] = saved_actions
343         # we don't allow more than 10 stored actions
344         if len(saved_actions["actions"]) >= 10:
345             del saved_actions["actions"][min(saved_actions["actions"].keys())]
346         key = saved_actions["next"]
347         saved_actions["actions"][key] = the_action
348         saved_actions["next"] = key + 1
349         return key
350
351     @openerpweb.jsonrequest
352     def get_session_action(self, req, key):
353         """
354         Gets back a previously saved action. This method can return None if the action
355         was saved since too much time (this case should be handled in a smart way).
356         
357         :param key: The key given by save_session_action()
358         :type key: integer
359         :return: The saved action or None.
360         :rtype: anything
361         """
362         saved_actions = cherrypy.session.get('saved_actions')
363         if not saved_actions:
364             return None
365         return saved_actions["actions"].get(key)
366
367 def eval_context_and_domain(session, context, domain=None):
368     e_context = session.eval_context(context)
369     # should we give the evaluated context as an evaluation context to the domain?
370     e_domain = session.eval_domain(domain or [])
371
372     return e_context, e_domain
373
374 def load_actions_from_ir_values(req, key, key2, models, meta, context):
375     Values = req.session.model('ir.values')
376     actions = Values.get(key, key2, models, meta, context)
377
378     return [(id, name, clean_action(action, req.session))
379             for id, name, action in actions]
380
381 def clean_action(action, session):
382     if action['type'] != 'ir.actions.act_window':
383         return action
384     # values come from the server, we can just eval them
385     if isinstance(action.get('context', None), basestring):
386         action['context'] = eval(
387             action['context'],
388             session.evaluation_context()) or {}
389
390     if isinstance(action.get('domain', None), basestring):
391         action['domain'] = eval(
392             action['domain'],
393             session.evaluation_context(
394                 action.get('context', {}))) or []
395     if 'flags' not in action:
396         # Set empty flags dictionary for web client.
397         action['flags'] = dict()
398     return fix_view_modes(action)
399
400 def generate_views(action):
401     """
402     While the server generates a sequence called "views" computing dependencies
403     between a bunch of stuff for views coming directly from the database
404     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
405     to return custom view dictionaries generated on the fly.
406
407     In that case, there is no ``views`` key available on the action.
408
409     Since the web client relies on ``action['views']``, generate it here from
410     ``view_mode`` and ``view_id``.
411
412     Currently handles two different cases:
413
414     * no view_id, multiple view_mode
415     * single view_id, single view_mode
416
417     :param dict action: action descriptor dictionary to generate a views key for
418     """
419     view_id = action.get('view_id', False)
420     if isinstance(view_id, (list, tuple)):
421         view_id = view_id[0]
422
423     # providing at least one view mode is a requirement, not an option
424     view_modes = action['view_mode'].split(',')
425
426     if len(view_modes) > 1:
427         if view_id:
428             raise ValueError('Non-db action dictionaries should provide '
429                              'either multiple view modes or a single view '
430                              'mode and an optional view id.\n\n Got view '
431                              'modes %r and view id %r for action %r' % (
432                 view_modes, view_id, action))
433         action['views'] = [(False, mode) for mode in view_modes]
434         return
435     action['views'] = [(view_id, view_modes[0])]
436
437 def fix_view_modes(action):
438     """ For historical reasons, OpenERP has weird dealings in relation to
439     view_mode and the view_type attribute (on window actions):
440
441     * one of the view modes is ``tree``, which stands for both list views
442       and tree views
443     * the choice is made by checking ``view_type``, which is either
444       ``form`` for a list view or ``tree`` for an actual tree view
445
446     This methods simply folds the view_type into view_mode by adding a
447     new view mode ``list`` which is the result of the ``tree`` view_mode
448     in conjunction with the ``form`` view_type.
449
450     TODO: this should go into the doc, some kind of "peculiarities" section
451
452     :param dict action: an action descriptor
453     :returns: nothing, the action is modified in place
454     """
455     if 'views' not in action:
456         generate_views(action)
457
458     if action.pop('view_type') != 'form':
459         return action
460
461     action['views'] = [
462         [id, mode if mode != 'tree' else 'list']
463         for id, mode in action['views']
464     ]
465
466     return action
467
468 class Menu(openerpweb.Controller):
469     _cp_path = "/base/menu"
470
471     @openerpweb.jsonrequest
472     def load(self, req):
473         return {'data': self.do_load(req)}
474
475     def do_load(self, req):
476         """ Loads all menu items (all applications and their sub-menus).
477
478         :param req: A request object, with an OpenERP session attribute
479         :type req: < session -> OpenERPSession >
480         :return: the menu root
481         :rtype: dict('children': menu_nodes)
482         """
483         Menus = req.session.model('ir.ui.menu')
484         # menus are loaded fully unlike a regular tree view, cause there are
485         # less than 512 items
486         context = req.session.eval_context(req.context)
487         menu_ids = Menus.search([], 0, False, False, context)
488         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
489         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
490         menu_items.append(menu_root)
491         
492         # make a tree using parent_id
493         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
494         for menu_item in menu_items:
495             if menu_item['parent_id']:
496                 parent = menu_item['parent_id'][0]
497             else:
498                 parent = False
499             if parent in menu_items_map:
500                 menu_items_map[parent].setdefault(
501                     'children', []).append(menu_item)
502
503         # sort by sequence a tree using parent_id
504         for menu_item in menu_items:
505             menu_item.setdefault('children', []).sort(
506                 key=lambda x:x["sequence"])
507
508         return menu_root
509
510     @openerpweb.jsonrequest
511     def action(self, req, menu_id):
512         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
513                                              [('ir.ui.menu', menu_id)], False,
514                                              req.session.eval_context(req.context))
515         return {"action": actions}
516
517 class DataSet(openerpweb.Controller):
518     _cp_path = "/base/dataset"
519
520     @openerpweb.jsonrequest
521     def fields(self, req, model):
522         return {'fields': req.session.model(model).fields_get(False,
523                                                               req.session.eval_context(req.context))}
524
525     @openerpweb.jsonrequest
526     def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
527         return self.do_search_read(request, model, fields, offset, limit, domain, sort)
528     def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
529                        , sort=None):
530         """ Performs a search() followed by a read() (if needed) using the
531         provided search criteria
532
533         :param request: a JSON-RPC request object
534         :type request: openerpweb.JsonRequest
535         :param str model: the name of the model to search on
536         :param fields: a list of the fields to return in the result records
537         :type fields: [str]
538         :param int offset: from which index should the results start being returned
539         :param int limit: the maximum number of records to return
540         :param list domain: the search domain for the query
541         :param list sort: sorting directives
542         :returns: A structure (dict) with two keys: ids (all the ids matching
543                   the (domain, context) pair) and records (paginated records
544                   matching fields selection set)
545         :rtype: list
546         """
547         Model = request.session.model(model)
548         context, domain = eval_context_and_domain(
549             request.session, request.context, domain)
550
551         ids = Model.search(domain, 0, False, sort or False, context)
552         # need to fill the dataset with all ids for the (domain, context) pair,
553         # so search un-paginated and paginate manually before reading
554         paginated_ids = ids[offset:(offset + limit if limit else None)]
555         if fields and fields == ['id']:
556             # shortcut read if we only want the ids
557             return {
558                 'ids': ids,
559                 'records': map(lambda id: {'id': id}, paginated_ids)
560             }
561
562         records = Model.read(paginated_ids, fields or False, context)
563         records.sort(key=lambda obj: ids.index(obj['id']))
564         return {
565             'ids': ids,
566             'records': records
567         }
568
569
570     @openerpweb.jsonrequest
571     def get(self, request, model, ids, fields=False):
572         return self.do_get(request, model, ids, fields)
573     def do_get(self, request, model, ids, fields=False):
574         """ Fetches and returns the records of the model ``model`` whose ids
575         are in ``ids``.
576
577         The results are in the same order as the inputs, but elements may be
578         missing (if there is no record left for the id)
579
580         :param request: the JSON-RPC2 request object
581         :type request: openerpweb.JsonRequest
582         :param model: the model to read from
583         :type model: str
584         :param ids: a list of identifiers
585         :type ids: list
586         :param fields: a list of fields to fetch, ``False`` or empty to fetch
587                        all fields in the model
588         :type fields: list | False
589         :returns: a list of records, in the same order as the list of ids
590         :rtype: list
591         """
592         Model = request.session.model(model)
593         records = Model.read(ids, fields, request.session.eval_context(request.context))
594
595         record_map = dict((record['id'], record) for record in records)
596
597         return [record_map[id] for id in ids if record_map.get(id)]
598     
599     @openerpweb.jsonrequest
600     def load(self, req, model, id, fields):
601         m = req.session.model(model)
602         value = {}
603         r = m.read([id], False, req.session.eval_context(req.context))
604         if r:
605             value = r[0]
606         return {'value': value}
607
608     @openerpweb.jsonrequest
609     def create(self, req, model, data):
610         m = req.session.model(model)
611         r = m.create(data, req.session.eval_context(req.context))
612         return {'result': r}
613
614     @openerpweb.jsonrequest
615     def save(self, req, model, id, data):
616         m = req.session.model(model)
617         r = m.write([id], data, req.session.eval_context(req.context))
618         return {'result': r}
619
620     @openerpweb.jsonrequest
621     def unlink(self, request, model, ids=()):
622         Model = request.session.model(model)
623         return Model.unlink(ids, request.session.eval_context(request.context))
624
625     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
626         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
627         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
628         c, d = eval_context_and_domain(req.session, context, domain)
629         if domain_id and len(args) - 1 >= domain_id:
630             args[domain_id] = d
631         if context_id and len(args) - 1 >= context_id:
632             args[context_id] = c
633
634         return getattr(req.session.model(model), method)(*args)
635
636     @openerpweb.jsonrequest
637     def call(self, req, model, method, args, domain_id=None, context_id=None):
638         return self.call_common(req, model, method, args, domain_id, context_id)
639
640     @openerpweb.jsonrequest
641     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
642         action = self.call_common(req, model, method, args, domain_id, context_id)
643         if isinstance(action, dict) and action.get('type') != '':
644             return {'result': clean_action(action, req.session)}
645         return {'result': False}
646
647     @openerpweb.jsonrequest
648     def exec_workflow(self, req, model, id, signal):
649         r = req.session.exec_workflow(model, id, signal)
650         return {'result': r}
651
652     @openerpweb.jsonrequest
653     def default_get(self, req, model, fields):
654         Model = req.session.model(model)
655         return Model.default_get(fields, req.session.eval_context(req.context))
656
657 class DataGroup(openerpweb.Controller):
658     _cp_path = "/base/group"
659     @openerpweb.jsonrequest
660     def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
661         Model = request.session.model(model)
662         context, domain = eval_context_and_domain(request.session, request.context, domain)
663
664         return Model.read_group(
665             domain or [], fields, group_by_fields, 0, False,
666             dict(context, group_by=group_by_fields), sort or False)
667
668 class View(openerpweb.Controller):
669     _cp_path = "/base/view"
670
671     def fields_view_get(self, request, model, view_id, view_type,
672                         transform=True, toolbar=False, submenu=False):
673         Model = request.session.model(model)
674         context = request.session.eval_context(request.context)
675         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
676         # todo fme?: check that we should pass the evaluated context here
677         self.process_view(request.session, fvg, context, transform)
678         return fvg
679
680     def process_view(self, session, fvg, context, transform):
681         # depending on how it feels, xmlrpclib.ServerProxy can translate
682         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
683         # enjoy unicode strings which can not be trivially converted to
684         # strings, and it blows up during parsing.
685
686         # So ensure we fix this retardation by converting view xml back to
687         # bit strings.
688         if isinstance(fvg['arch'], unicode):
689             arch = fvg['arch'].encode('utf-8')
690         else:
691             arch = fvg['arch']
692
693         if transform:
694             evaluation_context = session.evaluation_context(context or {})
695             xml = self.transform_view(arch, session, evaluation_context)
696         else:
697             xml = ElementTree.fromstring(arch)
698         fvg['arch'] = Xml2Json.convert_element(xml)
699
700         for field in fvg['fields'].itervalues():
701             if field.get('views'):
702                 for view in field["views"].itervalues():
703                     self.process_view(session, view, None, transform)
704             if field.get('domain'):
705                 field["domain"] = self.parse_domain(field["domain"], session)
706             if field.get('context'):
707                 field["context"] = self.parse_context(field["context"], session)
708
709     @openerpweb.jsonrequest
710     def add_custom(self, request, view_id, arch):
711         CustomView = request.session.model('ir.ui.view.custom')
712         CustomView.create({
713             'user_id': request.session._uid,
714             'ref_id': view_id,
715             'arch': arch
716         }, request.session.eval_context(request.context))
717         return {'result': True}
718
719     @openerpweb.jsonrequest
720     def undo_custom(self, request, view_id, reset=False):
721         CustomView = request.session.model('ir.ui.view.custom')
722         context = request.session.eval_context(request.context)
723         vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
724                                     0, False, False, context)
725         if vcustom:
726             if reset:
727                 CustomView.unlink(vcustom, context)
728             else:
729                 CustomView.unlink([vcustom[0]], context)
730             return {'result': True}
731         return {'result': False}
732
733     def transform_view(self, view_string, session, context=None):
734         # transform nodes on the fly via iterparse, instead of
735         # doing it statically on the parsing result
736         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
737         root = None
738         for event, elem in parser:
739             if event == "start":
740                 if root is None:
741                     root = elem
742                 self.parse_domains_and_contexts(elem, session)
743         return root
744
745     def parse_domain(self, domain, session):
746         """ Parses an arbitrary string containing a domain, transforms it
747         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
748
749         :param domain: the domain to parse, if the domain is not a string it is assumed to
750         be a literal domain and is returned as-is
751         :param session: Current OpenERP session
752         :type session: openerpweb.openerpweb.OpenERPSession
753         """
754         if not isinstance(domain, (str, unicode)):
755             return domain
756         try:
757             return openerpweb.ast.literal_eval(domain)
758         except ValueError:
759             # not a literal
760             return openerpweb.nonliterals.Domain(session, domain)
761         
762     def parse_context(self, context, session):
763         """ Parses an arbitrary string containing a context, transforms it
764         to either a literal context or a :class:`openerpweb.nonliterals.Context`
765
766         :param context: the context to parse, if the context is not a string it is assumed to
767         be a literal domain and is returned as-is
768         :param session: Current OpenERP session
769         :type session: openerpweb.openerpweb.OpenERPSession
770         """
771         if not isinstance(context, (str, unicode)):
772             return context
773         try:
774             return openerpweb.ast.literal_eval(context)
775         except ValueError:
776             return openerpweb.nonliterals.Context(session, context)
777
778     def parse_domains_and_contexts(self, elem, session):
779         """ Converts domains and contexts from the view into Python objects,
780         either literals if they can be parsed by literal_eval or a special
781         placeholder object if the domain or context refers to free variables.
782
783         :param elem: the current node being parsed
784         :type param: xml.etree.ElementTree.Element
785         :param session: OpenERP session object, used to store and retrieve
786                         non-literal objects
787         :type session: openerpweb.openerpweb.OpenERPSession
788         """
789         for el in ['domain', 'filter_domain']:
790             domain = elem.get(el, '').strip()
791             if domain:
792                 elem.set(el, self.parse_domain(domain, session))
793         for el in ['context', 'default_get']:
794             context_string = elem.get(el, '').strip()
795             if context_string:
796                 elem.set(el, self.parse_context(context_string, session))
797
798 class FormView(View):
799     _cp_path = "/base/formview"
800
801     @openerpweb.jsonrequest
802     def load(self, req, model, view_id, toolbar=False):
803         fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
804         return {'fields_view': fields_view}
805
806 class ListView(View):
807     _cp_path = "/base/listview"
808
809     @openerpweb.jsonrequest
810     def load(self, req, model, view_id, toolbar=False):
811         fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
812         return {'fields_view': fields_view}
813
814     def process_colors(self, view, row, context):
815         colors = view['arch']['attrs'].get('colors')
816
817         if not colors:
818             return None
819
820         color = [
821             pair.split(':')[0]
822             for pair in colors.split(';')
823             if eval(pair.split(':')[1], dict(context, **row))
824         ]
825
826         if not color:
827             return None
828         elif len(color) == 1:
829             return color[0]
830         return 'maroon'
831
832 class SearchView(View):
833     _cp_path = "/base/searchview"
834
835     @openerpweb.jsonrequest
836     def load(self, req, model, view_id):
837         fields_view = self.fields_view_get(req, model, view_id, 'search')
838         return {'fields_view': fields_view}
839
840     @openerpweb.jsonrequest
841     def fields_get(self, req, model):
842         Model = req.session.model(model)
843         fields = Model.fields_get(False, req.session.eval_context(req.context))
844         for field in fields.values():
845             # shouldn't convert the views too?
846             if field.get('domain'):
847                 field["domain"] = self.parse_domain(field["domain"], req.session)
848             if field.get('context'):
849                 field["context"] = self.parse_domain(field["context"], req.session)
850         return {'fields': fields}
851     
852     @openerpweb.jsonrequest
853     def get_filters(self, req, model):
854         Model = req.session.model("ir.filters")
855         filters = Model.get_filters(model)
856         for filter in filters:
857             filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
858             filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
859         return filters
860     
861     @openerpweb.jsonrequest
862     def save_filter(self, req, model, name, context_to_save, domain):
863         Model = req.session.model("ir.filters")
864         ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
865         ctx.session = req.session
866         ctx = ctx.evaluate()
867         domain = openerpweb.nonliterals.CompoundDomain(domain)
868         domain.session = req.session
869         domain = domain.evaluate()
870         uid = req.session._uid
871         context = req.session.eval_context(req.context)
872         to_return = Model.create_or_replace({"context": ctx,
873                                              "domain": domain,
874                                              "model_id": model,
875                                              "name": name,
876                                              "user_id": uid
877                                              }, context)
878         return to_return
879
880 class Binary(openerpweb.Controller):
881     _cp_path = "/base/binary"
882
883     @openerpweb.httprequest
884     def image(self, request, session_id, model, id, field, **kw):
885         cherrypy.response.headers['Content-Type'] = 'image/png'
886         Model = request.session.model(model)
887         context = request.session.eval_context(request.context)
888         try:
889             if not id:
890                 res = Model.default_get([field], context).get(field, '')
891             else:
892                 res = Model.read([int(id)], [field], context)[0].get(field, '')
893             return base64.decodestring(res)
894         except: # TODO: what's the exception here?
895             return self.placeholder()
896     def placeholder(self):
897         return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
898
899     @openerpweb.httprequest
900     def saveas(self, request, session_id, model, id, field, fieldname, **kw):
901         Model = request.session.model(model)
902         context = request.session.eval_context(request.context)
903         res = Model.read([int(id)], [field, fieldname], context)[0]
904         filecontent = res.get(field, '')
905         if not filecontent:
906             raise cherrypy.NotFound
907         else:
908             cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
909             filename = '%s_%s' % (model.replace('.', '_'), id)
910             if fieldname:
911                 filename = res.get(fieldname, '') or filename
912             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' +  filename
913             return base64.decodestring(filecontent)
914
915     @openerpweb.httprequest
916     def upload(self, request, session_id, callback, ufile=None):
917         cherrypy.response.timeout = 500
918         headers = {}
919         for key, val in cherrypy.request.headers.iteritems():
920             headers[key.lower()] = val
921         size = int(headers.get('content-length', 0))
922         # TODO: might be useful to have a configuration flag for max-length file uploads
923         try:
924             out = """<script language="javascript" type="text/javascript">
925                         var win = window.top.window,
926                             callback = win[%s];
927                         if (typeof(callback) === 'function') {
928                             callback.apply(this, %s);
929                         } else {
930                             win.jQuery('#oe_notification', win.document).notify('create', {
931                                 title: "Ajax File Upload",
932                                 text: "Could not find callback"
933                             });
934                         }
935                     </script>"""
936             data = ufile.file.read()
937             args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
938         except Exception, e:
939             args = [False, e.message]
940         return out % (simplejson.dumps(callback), simplejson.dumps(args))
941
942     @openerpweb.httprequest
943     def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
944         cherrypy.response.timeout = 500
945         context = request.session.eval_context(request.context)
946         Model = request.session.model('ir.attachment')
947         try:
948             out = """<script language="javascript" type="text/javascript">
949                         var win = window.top.window,
950                             callback = win[%s];
951                         if (typeof(callback) === 'function') {
952                             callback.call(this, %s);
953                         }
954                     </script>"""
955             attachment_id = Model.create({
956                 'name': ufile.filename,
957                 'datas': base64.encodestring(ufile.file.read()),
958                 'res_model': model,
959                 'res_id': int(id)
960             }, context)
961             args = {
962                 'filename': ufile.filename,
963                 'id':  attachment_id
964             }
965         except Exception, e:
966             args = { 'error': e.message }
967         return out % (simplejson.dumps(callback), simplejson.dumps(args))
968
969 class Action(openerpweb.Controller):
970     _cp_path = "/base/action"
971
972     @openerpweb.jsonrequest
973     def load(self, req, action_id):
974         Actions = req.session.model('ir.actions.actions')
975         value = False
976         context = req.session.eval_context(req.context)
977         action_type = Actions.read([action_id], ['type'], context)
978         if action_type:
979             action = req.session.model(action_type[0]['type']).read([action_id], False,
980                                                                     context)
981             if action:
982                 value = clean_action(action[0], req.session)
983         return {'result': value}
984
985     @openerpweb.jsonrequest
986     def run(self, req, action_id):
987         return clean_action(req.session.model('ir.actions.server').run(
988             [action_id], req.session.eval_context(req.context)), req.session)
989
990 #