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