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