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