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