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