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