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