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