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