[ADD] home action loading and execution
[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     @openerpweb.jsonrequest
236     def check(self, req):
237         req.session.assert_valid()
238         return None
239         
240 def eval_context_and_domain(session, context, domain=None):
241     e_context = session.eval_context(context)
242     # should we give the evaluated context as an evaluation context to the domain?
243     e_domain = session.eval_domain(domain or [])
244
245     return e_context, e_domain
246         
247 def load_actions_from_ir_values(req, key, key2, models, meta, context):
248     Values = req.session.model('ir.values')
249     actions = Values.get(key, key2, models, meta, context)
250
251     return [(id, name, clean_action(action, req.session))
252             for id, name, action in actions]
253
254 def clean_action(action, session):
255     if action['type'] != 'ir.actions.act_window':
256         return action
257     # values come from the server, we can just eval them
258     if isinstance(action.get('context', None), basestring):
259         action['context'] = eval(
260             action['context'],
261             session.evaluation_context()) or {}
262
263     if isinstance(action.get('domain', None), basestring):
264         action['domain'] = eval(
265             action['domain'],
266             session.evaluation_context(
267                 action['context'])) or []
268     if 'flags' not in action:
269         # Set empty flags dictionary for web client.
270         action['flags'] = dict()
271     return fix_view_modes(action)
272
273 def generate_views(action):
274     """
275     While the server generates a sequence called "views" computing dependencies
276     between a bunch of stuff for views coming directly from the database
277     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
278     to return custom view dictionaries generated on the fly.
279
280     In that case, there is no ``views`` key available on the action.
281
282     Since the web client relies on ``action['views']``, generate it here from
283     ``view_mode`` and ``view_id``.
284
285     Currently handles two different cases:
286
287     * no view_id, multiple view_mode
288     * single view_id, single view_mode
289
290     :param dict action: action descriptor dictionary to generate a views key for
291     """
292     view_id = action.get('view_id', False)
293     if isinstance(view_id, (list, tuple)):
294         view_id = view_id[0]
295
296     # providing at least one view mode is a requirement, not an option
297     view_modes = action['view_mode'].split(',')
298
299     if len(view_modes) > 1:
300         if view_id:
301             raise ValueError('Non-db action dictionaries should provide '
302                              'either multiple view modes or a single view '
303                              'mode and an optional view id.\n\n Got view '
304                              'modes %r and view id %r for action %r' % (
305                 view_modes, view_id, action))
306         action['views'] = [(False, mode) for mode in view_modes]
307         return
308     action['views'] = [(view_id, view_modes[0])]
309
310
311 def fix_view_modes(action):
312     """ For historical reasons, OpenERP has weird dealings in relation to
313     view_mode and the view_type attribute (on window actions):
314
315     * one of the view modes is ``tree``, which stands for both list views
316       and tree views
317     * the choice is made by checking ``view_type``, which is either
318       ``form`` for a list view or ``tree`` for an actual tree view
319
320     This methods simply folds the view_type into view_mode by adding a
321     new view mode ``list`` which is the result of the ``tree`` view_mode
322     in conjunction with the ``form`` view_type.
323
324     TODO: this should go into the doc, some kind of "peculiarities" section
325
326     :param dict action: an action descriptor
327     :returns: nothing, the action is modified in place
328     """
329     if 'views' not in action:
330         generate_views(action)
331
332     if action.pop('view_type') != 'form':
333         return
334
335     action['views'] = [
336         [id, mode if mode != 'tree' else 'list']
337         for id, mode in action['views']
338     ]
339
340     return action
341
342 class Menu(openerpweb.Controller):
343     _cp_path = "/base/menu"
344
345     @openerpweb.jsonrequest
346     def load(self, req):
347         return {'data': self.do_load(req)}
348
349     def do_load(self, req):
350         """ Loads all menu items (all applications and their sub-menus).
351
352         :param req: A request object, with an OpenERP session attribute
353         :type req: < session -> OpenERPSession >
354         :return: the menu root
355         :rtype: dict('children': menu_nodes)
356         """
357         Menus = req.session.model('ir.ui.menu')
358         # menus are loaded fully unlike a regular tree view, cause there are
359         # less than 512 items
360         context = req.session.eval_context(req.context)
361         menu_ids = Menus.search([], 0, False, False, context)
362         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
363         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
364         menu_items.append(menu_root)
365         
366         # make a tree using parent_id
367         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
368         for menu_item in menu_items:
369             if menu_item['parent_id']:
370                 parent = menu_item['parent_id'][0]
371             else:
372                 parent = False
373             if parent in menu_items_map:
374                 menu_items_map[parent].setdefault(
375                     'children', []).append(menu_item)
376
377         # sort by sequence a tree using parent_id
378         for menu_item in menu_items:
379             menu_item.setdefault('children', []).sort(
380                 key=lambda x:x["sequence"])
381
382         return menu_root
383
384     @openerpweb.jsonrequest
385     def action(self, req, menu_id):
386         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
387                                              [('ir.ui.menu', menu_id)], False,
388                                              req.session.eval_context(req.context))
389         return {"action": actions}
390
391 class DataSet(openerpweb.Controller):
392     _cp_path = "/base/dataset"
393
394     @openerpweb.jsonrequest
395     def fields(self, req, model):
396         return {'fields': req.session.model(model).fields_get(False,
397                                                               req.session.eval_context(req.context))}
398
399     @openerpweb.jsonrequest
400     def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
401         return self.do_search_read(request, model, fields, offset, limit, domain, context, sort)
402     def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, context=None, sort=None):
403         """ Performs a search() followed by a read() (if needed) using the
404         provided search criteria
405
406         :param request: a JSON-RPC request object
407         :type request: openerpweb.JsonRequest
408         :param str model: the name of the model to search on
409         :param fields: a list of the fields to return in the result records
410         :type fields: [str]
411         :param int offset: from which index should the results start being returned
412         :param int limit: the maximum number of records to return
413         :param list domain: the search domain for the query
414         :param list sort: sorting directives
415         :returns: a list of result records
416         :rtype: list
417         """
418         Model = request.session.model(model)
419         context, domain = eval_context_and_domain(request.session, request.context, domain)
420         
421         ids = Model.search(domain, offset or 0, limit or False,
422                            sort or False, context)
423
424         if fields and fields == ['id']:
425             # shortcut read if we only want the ids
426             return map(lambda id: {'id': id}, ids)
427
428         reads = Model.read(ids, fields or False, context)
429         reads.sort(key=lambda obj: ids.index(obj['id']))
430         return reads
431
432     @openerpweb.jsonrequest
433     def get(self, request, model, ids, fields=False):
434         return self.do_get(request, model, ids, fields)
435     def do_get(self, request, model, ids, fields=False):
436         """ Fetches and returns the records of the model ``model`` whose ids
437         are in ``ids``.
438
439         The results are in the same order as the inputs, but elements may be
440         missing (if there is no record left for the id)
441
442         :param request: the JSON-RPC2 request object
443         :type request: openerpweb.JsonRequest
444         :param model: the model to read from
445         :type model: str
446         :param ids: a list of identifiers
447         :type ids: list
448         :param fields: a list of fields to fetch, ``False`` or empty to fetch
449                        all fields in the model
450         :type fields: list | False
451         :returns: a list of records, in the same order as the list of ids
452         :rtype: list
453         """
454         Model = request.session.model(model)
455         records = Model.read(ids, fields, request.session.eval_context(request.context))
456
457         record_map = dict((record['id'], record) for record in records)
458
459         return [record_map[id] for id in ids if record_map.get(id)]
460     
461     @openerpweb.jsonrequest
462     def load(self, req, model, id, fields):
463         m = req.session.model(model)
464         value = {}
465         r = m.read([id], False, req.session.eval_context(req.context))
466         if r:
467             value = r[0]
468         return {'value': value}
469
470     @openerpweb.jsonrequest
471     def create(self, req, model, data):
472         m = req.session.model(model)
473         r = m.create(data, req.session.eval_context(req.context))
474         return {'result': r}
475
476     @openerpweb.jsonrequest
477     def save(self, req, model, id, data):
478         m = req.session.model(model)
479         r = m.write([id], data, req.session.eval_context(req.context))
480         return {'result': r}
481
482     @openerpweb.jsonrequest
483     def unlink(self, request, model, ids=()):
484         Model = request.session.model(model)
485         return Model.unlink(ids, request.session.eval_context(request.context))
486
487     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
488         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
489         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
490         c, d = eval_context_and_domain(req.session, context, domain)
491         if domain_id and len(args) - 1 >= domain_id:
492             args[domain_id] = d
493         if context_id and len(args) - 1 >= context_id:
494             args[context_id] = c
495
496         return getattr(req.session.model(model), method)(*args)
497
498     @openerpweb.jsonrequest
499     def call(self, req, model, method, args, domain_id=None, context_id=None):
500         return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
501
502     @openerpweb.jsonrequest
503     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
504         action = self.call_common(req, model, method, args, domain_id, context_id)
505         if isinstance(action, dict) and action.get('type') != '':
506             return {'result': clean_action(action, req.session)}
507         return {'result': False}
508
509     @openerpweb.jsonrequest
510     def exec_workflow(self, req, model, id, signal):
511         r = req.session.exec_workflow(model, id, signal)
512         return {'result': r}
513
514     @openerpweb.jsonrequest
515     def default_get(self, req, model, fields):
516         m = req.session.model(model)
517         r = m.default_get(fields, req.session.eval_context(req.context))
518         return {'result': r}
519
520 class DataGroup(openerpweb.Controller):
521     _cp_path = "/base/group"
522     @openerpweb.jsonrequest
523     def read(self, request, model, group_by_fields, domain=None):
524         Model = request.session.model(model)
525         context, domain = eval_context_and_domain(request.session, request.context, domain)
526
527         return Model.read_group(
528             domain or [], False, group_by_fields, 0, False,
529             dict(context, group_by=group_by_fields))
530
531 class View(openerpweb.Controller):
532     _cp_path = "/base/view"
533
534     def fields_view_get(self, request, model, view_id, view_type,
535                         transform=True, toolbar=False, submenu=False):
536         Model = request.session.model(model)
537         context = request.session.eval_context(request.context)
538         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
539         # todo fme?: check that we should pass the evaluated context here
540         self.process_view(request.session, fvg, context, transform)
541         return fvg
542     
543     def process_view(self, session, fvg, context, transform):
544         # depending on how it feels, xmlrpclib.ServerProxy can translate
545         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
546         # enjoy unicode strings which can not be trivially converted to
547         # strings, and it blows up during parsing.
548
549         # So ensure we fix this retardation by converting view xml back to
550         # bit strings.
551         if isinstance(fvg['arch'], unicode):
552             arch = fvg['arch'].encode('utf-8')
553         else:
554             arch = fvg['arch']
555
556         if transform:
557             evaluation_context = session.evaluation_context(context or {})
558             xml = self.transform_view(arch, session, evaluation_context)
559         else:
560             xml = ElementTree.fromstring(arch)
561         fvg['arch'] = Xml2Json.convert_element(xml)
562         for field in fvg['fields'].values():
563             if field.has_key('views') and field['views']:
564                 for view in field["views"].values():
565                     self.process_view(session, view, None, transform)
566
567     @openerpweb.jsonrequest
568     def add_custom(self, request, view_id, arch):
569         CustomView = request.session.model('ir.ui.view.custom')
570         CustomView.create({
571             'user_id': request.session._uid,
572             'ref_id': view_id,
573             'arch': arch
574         }, request.session.eval_context(request.context))
575         return {'result': True}
576
577     @openerpweb.jsonrequest
578     def undo_custom(self, request, view_id, reset=False):
579         CustomView = request.session.model('ir.ui.view.custom')
580         context = request.session.eval_context(request.context)
581         vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
582                                     0, False, False, context)
583         if vcustom:
584             if reset:
585                 CustomView.unlink(vcustom, context)
586             else:
587                 CustomView.unlink([vcustom[0]], context)
588             return {'result': True}
589         return {'result': False}
590
591     def normalize_attrs(self, elem, context):
592         """ Normalize @attrs, @invisible, @required, @readonly and @states, so
593         the client only has to deal with @attrs.
594
595         See `the discoveries pad <http://pad.openerp.com/discoveries>`_ for
596         the rationale.
597
598         :param elem: the current view node (Python object)
599         :type elem: xml.etree.ElementTree.Element
600         :param dict context: evaluation context
601         """
602         # If @attrs is normalized in json by server, the eval should be replaced by simplejson.loads
603         attrs = openerpweb.ast.literal_eval(elem.get('attrs', '{}'))
604         if 'states' in elem.attrib:
605             attrs.setdefault('invisible', [])\
606                 .append(('state', 'not in', elem.attrib.pop('states').split(',')))
607         if attrs:
608             elem.set('attrs', simplejson.dumps(attrs))
609         for a in ['invisible', 'readonly', 'required']:
610             if a in elem.attrib:
611                 # In the XML we trust
612                 avalue = bool(eval(elem.get(a, 'False'),
613                                    {'context': context or {}}))
614                 if not avalue:
615                     del elem.attrib[a]
616                 else:
617                     elem.attrib[a] = '1'
618                     if a == 'invisible' and 'attrs' in elem.attrib:
619                         del elem.attrib['attrs']
620
621     def transform_view(self, view_string, session, context=None):
622         # transform nodes on the fly via iterparse, instead of
623         # doing it statically on the parsing result
624         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
625         root = None
626         for event, elem in parser:
627             if event == "start":
628                 if root is None:
629                     root = elem
630                 self.normalize_attrs(elem, context)
631                 self.parse_domains_and_contexts(elem, session)
632         return root
633
634     def parse_domain(self, elem, attr_name, session):
635         """ Parses an attribute of the provided name as a domain, transforms it
636         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
637
638         :param elem: the node being parsed
639         :type param: xml.etree.ElementTree.Element
640         :param str attr_name: the name of the attribute which should be parsed
641         :param session: Current OpenERP session
642         :type session: openerpweb.openerpweb.OpenERPSession
643         """
644         domain = elem.get(attr_name, '').strip()
645         if domain:
646             try:
647                 elem.set(
648                     attr_name,
649                     openerpweb.ast.literal_eval(
650                         domain))
651             except ValueError:
652                 # not a literal
653                 elem.set(attr_name,
654                          openerpweb.nonliterals.Domain(session, domain))
655
656     def parse_domains_and_contexts(self, elem, session):
657         """ Converts domains and contexts from the view into Python objects,
658         either literals if they can be parsed by literal_eval or a special
659         placeholder object if the domain or context refers to free variables.
660
661         :param elem: the current node being parsed
662         :type param: xml.etree.ElementTree.Element
663         :param session: OpenERP session object, used to store and retrieve
664                         non-literal objects
665         :type session: openerpweb.openerpweb.OpenERPSession
666         """
667         self.parse_domain(elem, 'domain', session)
668         self.parse_domain(elem, 'filter_domain', session)
669         for el in ['context', 'default_get']:
670             context_string = elem.get(el, '').strip()
671             if context_string:
672                 try:
673                     elem.set(el,
674                              openerpweb.ast.literal_eval(context_string))
675                 except ValueError:
676                     elem.set(el,
677                              openerpweb.nonliterals.Context(
678                                  session, context_string))
679
680 class FormView(View):
681     _cp_path = "/base/formview"
682
683     @openerpweb.jsonrequest
684     def load(self, req, model, view_id, toolbar=False):
685         fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
686         return {'fields_view': fields_view}
687
688 class ListView(View):
689     _cp_path = "/base/listview"
690
691     @openerpweb.jsonrequest
692     def load(self, req, model, view_id, toolbar=False):
693         fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
694         return {'fields_view': fields_view}
695
696     def process_colors(self, view, row, context):
697         colors = view['arch']['attrs'].get('colors')
698
699         if not colors:
700             return None
701
702         color = [
703             pair.split(':')[0]
704             for pair in colors.split(';')
705             if eval(pair.split(':')[1], dict(context, **row))
706         ]
707
708         if not color:
709             return None
710         elif len(color) == 1:
711             return color[0]
712         return 'maroon'
713
714 class SearchView(View):
715     _cp_path = "/base/searchview"
716
717     @openerpweb.jsonrequest
718     def load(self, req, model, view_id):
719         fields_view = self.fields_view_get(req, model, view_id, 'search')
720         return {'fields_view': fields_view}
721
722     @openerpweb.jsonrequest
723     def fields_get(self, req, model):
724         Model = req.session.model(model)
725         fields = Model.fields_get(False, req.session.eval_context(req.context))
726         return {'fields': fields}
727
728 class Binary(openerpweb.Controller):
729     _cp_path = "/base/binary"
730
731     @openerpweb.httprequest
732     def image(self, request, session_id, model, id, field, **kw):
733         cherrypy.response.headers['Content-Type'] = 'image/png'
734         Model = request.session.model(model)
735         context = request.session.eval_context(request.context)
736         try:
737             if not id:
738                 res = Model.default_get([field], context).get(field, '')
739             else:
740                 res = Model.read([int(id)], [field], context)[0].get(field, '')
741             return base64.decodestring(res)
742         except: # TODO: what's the exception here?
743             return self.placeholder()
744     def placeholder(self):
745         return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
746
747     @openerpweb.httprequest
748     def saveas(self, request, session_id, model, id, field, fieldname, **kw):
749         Model = request.session.model(model)
750         context = request.session.eval_context(request.context)
751         res = Model.read([int(id)], [field, fieldname], context)[0]
752         filecontent = res.get(field, '')
753         if not filecontent:
754             raise cherrypy.NotFound
755         else:
756             cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
757             filename = '%s_%s' % (model.replace('.', '_'), id)
758             if fieldname:
759                 filename = res.get(fieldname, '') or filename
760             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' +  filename
761             return base64.decodestring(filecontent)
762
763     @openerpweb.httprequest
764     def upload(self, request, session_id, callback, ufile=None):
765         cherrypy.response.timeout = 500
766         headers = {}
767         for key, val in cherrypy.request.headers.iteritems():
768             headers[key.lower()] = val
769         size = int(headers.get('content-length', 0))
770         # TODO: might be useful to have a configuration flag for max-length file uploads
771         try:
772             out = """<script language="javascript" type="text/javascript">
773                         var win = window.top.window,
774                             callback = win[%s];
775                         if (typeof(callback) === 'function') {
776                             callback.apply(this, %s);
777                         } else {
778                             win.jQuery('#oe_notification', win.document).notify('create', {
779                                 title: "Ajax File Upload",
780                                 text: "Could not find callback"
781                             });
782                         }
783                     </script>"""
784             data = ufile.file.read()
785             args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
786         except Exception, e:
787             args = [False, e.message]
788         return out % (simplejson.dumps(callback), simplejson.dumps(args))
789
790     @openerpweb.httprequest
791     def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
792         cherrypy.response.timeout = 500
793         context = request.session.eval_context(request.context)
794         Model = request.session.model('ir.attachment')
795         try:
796             out = """<script language="javascript" type="text/javascript">
797                         var win = window.top.window,
798                             callback = win[%s];
799                         if (typeof(callback) === 'function') {
800                             callback.call(this, %s);
801                         }
802                     </script>"""
803             attachment_id = Model.create({
804                 'name': ufile.filename,
805                 'datas': base64.encodestring(ufile.file.read()),
806                 'res_model': model,
807                 'res_id': int(id)
808             }, context)
809             args = {
810                 'filename': ufile.filename,
811                 'id':  attachment_id
812             }
813         except Exception, e:
814             args = { 'error': e.message }
815         return out % (simplejson.dumps(callback), simplejson.dumps(args))
816
817 class Action(openerpweb.Controller):
818     _cp_path = "/base/action"
819
820     @openerpweb.jsonrequest
821     def load(self, req, action_id):
822         Actions = req.session.model('ir.actions.actions')
823         value = False
824         context = req.session.eval_context(req.context)
825         action_type = Actions.read([action_id], ['type'], context)
826         if action_type:
827             action = req.session.model(action_type[0]['type']).read([action_id], False,
828                                                                     context)
829             if action:
830                 value = clean_action(action[0], req.session)
831         return {'result': value}
832
833     @openerpweb.jsonrequest
834     def run(self, req, action_id):
835         return clean_action(req.session.model('ir.actions.server').run(
836             [action_id], req.session.eval_context(req.context)), req.session)