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