[MERGE] list pagination branch
[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 def eval_context_and_domain(session, context, domain=None):
235     e_context = session.eval_context(context)
236     # should we give the evaluated context as an evaluation context to the domain?
237     e_domain = session.eval_domain(domain or [])
238
239     return e_context, e_domain
240         
241 def load_actions_from_ir_values(req, key, key2, models, meta, context):
242     Values = req.session.model('ir.values')
243     actions = Values.get(key, key2, models, meta, context)
244
245     return [(id, name, clean_action(action, req.session))
246             for id, name, action in actions]
247
248 def clean_action(action, session):
249     if action['type'] != 'ir.actions.act_window':
250         return action
251     # values come from the server, we can just eval them
252     if isinstance(action.get('context', None), basestring):
253         action['context'] = eval(
254             action['context'],
255             session.evaluation_context()) or {}
256
257     if isinstance(action.get('domain', None), basestring):
258         action['domain'] = eval(
259             action['domain'],
260             session.evaluation_context(
261                 action['context'])) or []
262     if 'flags' not in action:
263         # Set empty flags dictionary for web client.
264         action['flags'] = dict()
265     return fix_view_modes(action)
266
267 def generate_views(action):
268     """
269     While the server generates a sequence called "views" computing dependencies
270     between a bunch of stuff for views coming directly from the database
271     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
272     to return custom view dictionaries generated on the fly.
273
274     In that case, there is no ``views`` key available on the action.
275
276     Since the web client relies on ``action['views']``, generate it here from
277     ``view_mode`` and ``view_id``.
278
279     Currently handles two different cases:
280
281     * no view_id, multiple view_mode
282     * single view_id, single view_mode
283
284     :param dict action: action descriptor dictionary to generate a views key for
285     """
286     view_id = action.get('view_id', False)
287     if isinstance(view_id, (list, tuple)):
288         view_id = view_id[0]
289
290     # providing at least one view mode is a requirement, not an option
291     view_modes = action['view_mode'].split(',')
292
293     if len(view_modes) > 1:
294         if view_id:
295             raise ValueError('Non-db action dictionaries should provide '
296                              'either multiple view modes or a single view '
297                              'mode and an optional view id.\n\n Got view '
298                              'modes %r and view id %r for action %r' % (
299                 view_modes, view_id, action))
300         action['views'] = [(False, mode) for mode in view_modes]
301         return
302     action['views'] = [(view_id, view_modes[0])]
303
304
305 def fix_view_modes(action):
306     """ For historical reasons, OpenERP has weird dealings in relation to
307     view_mode and the view_type attribute (on window actions):
308
309     * one of the view modes is ``tree``, which stands for both list views
310       and tree views
311     * the choice is made by checking ``view_type``, which is either
312       ``form`` for a list view or ``tree`` for an actual tree view
313
314     This methods simply folds the view_type into view_mode by adding a
315     new view mode ``list`` which is the result of the ``tree`` view_mode
316     in conjunction with the ``form`` view_type.
317
318     TODO: this should go into the doc, some kind of "peculiarities" section
319
320     :param dict action: an action descriptor
321     :returns: nothing, the action is modified in place
322     """
323     if 'views' not in action:
324         generate_views(action)
325
326     if action.pop('view_type') != 'form':
327         return
328
329     action['views'] = [
330         [id, mode if mode != 'tree' else 'list']
331         for id, mode in action['views']
332     ]
333
334     return action
335
336 class Menu(openerpweb.Controller):
337     _cp_path = "/base/menu"
338
339     @openerpweb.jsonrequest
340     def load(self, req):
341         return {'data': self.do_load(req)}
342
343     def do_load(self, req):
344         """ Loads all menu items (all applications and their sub-menus).
345
346         :param req: A request object, with an OpenERP session attribute
347         :type req: < session -> OpenERPSession >
348         :return: the menu root
349         :rtype: dict('children': menu_nodes)
350         """
351         Menus = req.session.model('ir.ui.menu')
352         # menus are loaded fully unlike a regular tree view, cause there are
353         # less than 512 items
354         context = req.session.eval_context(req.context)
355         menu_ids = Menus.search([], 0, False, False, context)
356         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
357         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
358         menu_items.append(menu_root)
359         
360         # make a tree using parent_id
361         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
362         for menu_item in menu_items:
363             if menu_item['parent_id']:
364                 parent = menu_item['parent_id'][0]
365             else:
366                 parent = False
367             if parent in menu_items_map:
368                 menu_items_map[parent].setdefault(
369                     'children', []).append(menu_item)
370
371         # sort by sequence a tree using parent_id
372         for menu_item in menu_items:
373             menu_item.setdefault('children', []).sort(
374                 key=lambda x:x["sequence"])
375
376         return menu_root
377
378     @openerpweb.jsonrequest
379     def action(self, req, menu_id):
380         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
381                                              [('ir.ui.menu', menu_id)], False,
382                                              req.session.eval_context(req.context))
383         return {"action": actions}
384
385 class DataSet(openerpweb.Controller):
386     _cp_path = "/base/dataset"
387
388     @openerpweb.jsonrequest
389     def fields(self, req, model):
390         return {'fields': req.session.model(model).fields_get(False,
391                                                               req.session.eval_context(req.context))}
392
393     @openerpweb.jsonrequest
394     def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
395         return self.do_search_read(request, model, fields, offset, limit, domain, sort)
396     def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
397                        , 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 structure (dict) with two keys: ids (all the ids matching
411                   the (domain, context) pair) and records (paginated records
412                   matching fields selection set)
413         :rtype: list
414         """
415         Model = request.session.model(model)
416         context, domain = eval_context_and_domain(
417             request.session, request.context, domain)
418
419         ids = Model.search(domain, 0, False, sort or False, context)
420         # need to fill the dataset with all ids for the (domain, context) pair,
421         # so search un-paginated and paginate manually before reading
422         paginated_ids = ids[offset:(offset + limit if limit else None)]
423         if fields and fields == ['id']:
424             # shortcut read if we only want the ids
425             return {
426                 'ids': ids,
427                 'records': map(lambda id: {'id': id}, paginated_ids)
428             }
429
430         records = Model.read(paginated_ids, fields or False, context)
431         records.sort(key=lambda obj: ids.index(obj['id']))
432         return {
433             'ids': ids,
434             'records': records
435         }
436
437
438     @openerpweb.jsonrequest
439     def get(self, request, model, ids, fields=False):
440         return self.do_get(request, model, ids, fields)
441     def do_get(self, request, model, ids, fields=False):
442         """ Fetches and returns the records of the model ``model`` whose ids
443         are in ``ids``.
444
445         The results are in the same order as the inputs, but elements may be
446         missing (if there is no record left for the id)
447
448         :param request: the JSON-RPC2 request object
449         :type request: openerpweb.JsonRequest
450         :param model: the model to read from
451         :type model: str
452         :param ids: a list of identifiers
453         :type ids: list
454         :param fields: a list of fields to fetch, ``False`` or empty to fetch
455                        all fields in the model
456         :type fields: list | False
457         :returns: a list of records, in the same order as the list of ids
458         :rtype: list
459         """
460         Model = request.session.model(model)
461         records = Model.read(ids, fields, request.session.eval_context(request.context))
462
463         record_map = dict((record['id'], record) for record in records)
464
465         return [record_map[id] for id in ids if record_map.get(id)]
466     
467     @openerpweb.jsonrequest
468     def load(self, req, model, id, fields):
469         m = req.session.model(model)
470         value = {}
471         r = m.read([id], False, req.session.eval_context(req.context))
472         if r:
473             value = r[0]
474         return {'value': value}
475
476     @openerpweb.jsonrequest
477     def create(self, req, model, data):
478         m = req.session.model(model)
479         r = m.create(data, req.session.eval_context(req.context))
480         return {'result': r}
481
482     @openerpweb.jsonrequest
483     def save(self, req, model, id, data):
484         m = req.session.model(model)
485         r = m.write([id], data, req.session.eval_context(req.context))
486         return {'result': r}
487
488     @openerpweb.jsonrequest
489     def unlink(self, request, model, ids=()):
490         Model = request.session.model(model)
491         return Model.unlink(ids, request.session.eval_context(request.context))
492
493     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
494         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
495         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
496         c, d = eval_context_and_domain(req.session, context, domain)
497         if domain_id and len(args) - 1 >= domain_id:
498             args[domain_id] = d
499         if context_id and len(args) - 1 >= context_id:
500             args[context_id] = c
501
502         return getattr(req.session.model(model), method)(*args)
503
504     @openerpweb.jsonrequest
505     def call(self, req, model, method, args, domain_id=None, context_id=None):
506         return {'result': self.call_common(req, model, method, args, domain_id, context_id)}
507
508     @openerpweb.jsonrequest
509     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
510         action = self.call_common(req, model, method, args, domain_id, context_id)
511         if isinstance(action, dict) and action.get('type') != '':
512             return {'result': clean_action(action, req.session)}
513         return {'result': False}
514
515     @openerpweb.jsonrequest
516     def exec_workflow(self, req, model, id, signal):
517         r = req.session.exec_workflow(model, id, signal)
518         return {'result': r}
519
520     @openerpweb.jsonrequest
521     def default_get(self, req, model, fields):
522         m = req.session.model(model)
523         r = m.default_get(fields, req.session.eval_context(req.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     @openerpweb.jsonrequest
840     def run(self, req, action_id):
841         return clean_action(req.session.model('ir.actions.server').run(
842             [action_id], req.session.eval_context(req.context)), req.session)