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