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