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