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