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