[MERGE] Merge with trunk
[odoo/odoo.git] / addons / base / controllers / main.py
1 # -*- coding: utf-8 -*-
2 import base64, glob, os, re
3 from xml.etree import ElementTree
4 from cStringIO import StringIO
5
6 import simplejson
7
8 import openerpweb
9 import openerpweb.ast
10 import openerpweb.nonliterals
11
12 import cherrypy
13 import csv
14 import xml.dom.minidom
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 def manifest_glob(addons, key):
60     files = []
61     for addon in addons:
62         globlist = openerpweb.addons_manifest.get(addon, {}).get(key, [])
63         print globlist
64         for pattern in globlist:
65             for path in glob.glob(os.path.join(openerpweb.path_addons, addon, pattern)):
66                 files.append(path[len(openerpweb.path_addons):])
67     return files
68
69 def concat_files(file_list):
70     """ Concatenate file content
71     return (concat,timestamp)
72     concat: concatenation of file content
73     timestamp: max(os.path.getmtime of file_list)
74     """
75     root = openerpweb.path_root
76     files_content = []
77     files_timestamp = 0
78     for i in file_list:
79         fname = os.path.join(root, i)
80         ftime = os.path.getmtime(fname)
81         if ftime > files_timestamp:
82             files_timestamp = ftime
83         files_content = open(fname).read()
84     files_concat = "".join(files_content)
85     return files_concat
86
87 class WebClient(openerpweb.Controller):
88     _cp_path = "/base/webclient"
89
90     @openerpweb.jsonrequest
91     def csslist(self, req, mods='base'):
92         return manifest_glob(mods.split(','), 'css')
93
94     @openerpweb.jsonrequest
95     def jslist(self, req, mods='base'):
96         return manifest_glob(mods.split(','), 'js')
97
98     @openerpweb.httprequest
99     def css(self, req, mods='base'):
100         cherrypy.response.headers['Content-Type'] = 'text/css'
101         files = manifest_glob(mods.split(','), 'css')
102         concat = concat_files(files)[0]
103         # TODO request set the Date of last modif and Etag
104         return concat
105
106     @openerpweb.httprequest
107     def js(self, req, mods='base'):
108         cherrypy.response.headers['Content-Type'] = 'application/javascript'
109         files = manifest_glob(mods.split(','), 'js')
110         concat = concat_files(files)[0]
111         # TODO request set the Date of last modif and Etag
112         return concat
113
114     @openerpweb.httprequest
115     def home(self, req):
116         template ="""<!DOCTYPE html>
117         <html style="height: 100%%">
118         <head>
119             <meta http-equiv="content-type" content="text/html; charset=utf-8" />
120             <title>OpenERP</title>
121             %s
122             <script type="text/javascript">
123             $(function() {
124                 QWeb = new QWeb2.Engine(); 
125                 openerp.init().base.webclient("oe"); 
126             });
127             </script>
128             <link rel="shortcut icon" href="/base/static/src/img/favicon.ico" type="image/x-icon"/>
129             %s
130             <!--[if lte IE 7]>
131             <link rel="stylesheet" href="/base/static/src/css/base-ie7.css" type="text/css"/>
132             <![endif]-->
133         </head>
134         <body id="oe" class="openerp"></body>
135         </html>
136         """.replace('\n'+' '*8,'\n')
137
138         # script tags
139         jslist = ['/base/webclient/js']
140         if 1: # debug == 1
141             jslist = manifest_glob(['base'], 'js')
142         js = "\n    ".join(['<script type="text/javascript" src="%s"></script>'%i for i in jslist])
143
144         # css tags
145         csslist = ['/base/webclient/css']
146         if 1: # debug == 1
147             csslist = manifest_glob(['base'], 'css')
148         css = "\n    ".join(['<link rel="stylesheet" href="%s">'%i for i in csslist])
149         r = template % (js, css)
150         return r
151
152 class Database(openerpweb.Controller):
153     _cp_path = "/base/database"
154
155     @openerpweb.jsonrequest
156     def get_databases_list(self, req):
157         proxy = req.session.proxy("db")
158         dbs = proxy.list()
159         h = req.httprequest.headers['Host'].split(':')[0]
160         d = h.split('.')[0]
161         r = cherrypy.config['openerp.dbfilter'].replace('%h',h).replace('%d',d)
162         print "h,d",h,d,r
163         dbs = [i for i in dbs if re.match(r,i)]
164         return {"db_list": dbs}
165
166 class Session(openerpweb.Controller):
167     _cp_path = "/base/session"
168
169     @openerpweb.jsonrequest
170     def login(self, req, db, login, password):
171         req.session.login(db, login, password)
172
173         return {
174             "session_id": req.session_id,
175             "uid": req.session._uid,
176         }
177
178     @openerpweb.jsonrequest
179     def sc_list(self, req):
180         return req.session.model('ir.ui.view_sc').get_sc(req.session._uid, "ir.ui.menu",
181                                                          req.session.eval_context(req.context))
182
183     @openerpweb.jsonrequest
184     def modules(self, req):
185         # TODO query server for installed web modules
186         mods = []
187         for name, manifest in openerpweb.addons_manifest.items():
188             if name != 'base' and manifest.get('active', True):
189                 mods.append(name)
190         return mods
191
192     @openerpweb.jsonrequest
193     def eval_domain_and_context(self, req, contexts, domains,
194                                 group_by_seq=None):
195         """ Evaluates sequences of domains and contexts, composing them into
196         a single context, domain or group_by sequence.
197
198         :param list contexts: list of contexts to merge together. Contexts are
199                               evaluated in sequence, all previous contexts
200                               are part of their own evaluation context
201                               (starting at the session context).
202         :param list domains: list of domains to merge together. Domains are
203                              evaluated in sequence and appended to one another
204                              (implicit AND), their evaluation domain is the
205                              result of merging all contexts.
206         :param list group_by_seq: list of domains (which may be in a different
207                                   order than the ``contexts`` parameter),
208                                   evaluated in sequence, their ``'group_by'``
209                                   key is extracted if they have one.
210         :returns:
211             a 3-dict of:
212
213             context (``dict``)
214                 the global context created by merging all of
215                 ``contexts``
216
217             domain (``list``)
218                 the concatenation of all domains
219
220             group_by (``list``)
221                 a list of fields to group by, potentially empty (in which case
222                 no group by should be performed)
223         """
224         context, domain = eval_context_and_domain(req.session,
225                                                   openerpweb.nonliterals.CompoundContext(*(contexts or [])),
226                                                   openerpweb.nonliterals.CompoundDomain(*(domains or [])))
227
228         group_by_sequence = []
229         for candidate in (group_by_seq or []):
230             ctx = req.session.eval_context(candidate, context)
231             group_by = ctx.get('group_by')
232             if not group_by:
233                 continue
234             elif isinstance(group_by, basestring):
235                 group_by_sequence.append(group_by)
236             else:
237                 group_by_sequence.extend(group_by)
238
239         return {
240             'context': context,
241             'domain': domain,
242             'group_by': group_by_sequence
243         }
244
245     @openerpweb.jsonrequest
246     def save_session_action(self, req, the_action):
247         """
248         This method store an action object in the session object and returns an integer
249         identifying that action. The method get_session_action() can be used to get
250         back the action.
251
252         :param the_action: The action to save in the session.
253         :type the_action: anything
254         :return: A key identifying the saved action.
255         :rtype: integer
256         """
257         saved_actions = cherrypy.session.get('saved_actions')
258         if not saved_actions:
259             saved_actions = {"next":0, "actions":{}}
260             cherrypy.session['saved_actions'] = saved_actions
261         # we don't allow more than 10 stored actions
262         if len(saved_actions["actions"]) >= 10:
263             del saved_actions["actions"][min(saved_actions["actions"].keys())]
264         key = saved_actions["next"]
265         saved_actions["actions"][key] = the_action
266         saved_actions["next"] = key + 1
267         return key
268
269     @openerpweb.jsonrequest
270     def get_session_action(self, req, key):
271         """
272         Gets back a previously saved action. This method can return None if the action
273         was saved since too much time (this case should be handled in a smart way).
274
275         :param key: The key given by save_session_action()
276         :type key: integer
277         :return: The saved action or None.
278         :rtype: anything
279         """
280         saved_actions = cherrypy.session.get('saved_actions')
281         if not saved_actions:
282             return None
283         return saved_actions["actions"].get(key)
284
285 def eval_context_and_domain(session, context, domain=None):
286     e_context = session.eval_context(context)
287     # should we give the evaluated context as an evaluation context to the domain?
288     e_domain = session.eval_domain(domain or [])
289
290     return e_context, e_domain
291
292 def load_actions_from_ir_values(req, key, key2, models, meta, context):
293     Values = req.session.model('ir.values')
294     actions = Values.get(key, key2, models, meta, context)
295
296     return [(id, name, clean_action(action, req.session))
297             for id, name, action in actions]
298
299 def clean_action(action, session):
300     if action['type'] != 'ir.actions.act_window':
301         return action
302     # values come from the server, we can just eval them
303     if isinstance(action.get('context', None), basestring):
304         action['context'] = eval(
305             action['context'],
306             session.evaluation_context()) or {}
307
308     if isinstance(action.get('domain', None), basestring):
309         action['domain'] = eval(
310             action['domain'],
311             session.evaluation_context(
312                 action.get('context', {}))) or []
313     if 'flags' not in action:
314         # Set empty flags dictionary for web client.
315         action['flags'] = dict()
316     return fix_view_modes(action)
317
318 def generate_views(action):
319     """
320     While the server generates a sequence called "views" computing dependencies
321     between a bunch of stuff for views coming directly from the database
322     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
323     to return custom view dictionaries generated on the fly.
324
325     In that case, there is no ``views`` key available on the action.
326
327     Since the web client relies on ``action['views']``, generate it here from
328     ``view_mode`` and ``view_id``.
329
330     Currently handles two different cases:
331
332     * no view_id, multiple view_mode
333     * single view_id, single view_mode
334
335     :param dict action: action descriptor dictionary to generate a views key for
336     """
337     view_id = action.get('view_id', False)
338     if isinstance(view_id, (list, tuple)):
339         view_id = view_id[0]
340
341     # providing at least one view mode is a requirement, not an option
342     view_modes = action['view_mode'].split(',')
343
344     if len(view_modes) > 1:
345         if view_id:
346             raise ValueError('Non-db action dictionaries should provide '
347                              'either multiple view modes or a single view '
348                              'mode and an optional view id.\n\n Got view '
349                              'modes %r and view id %r for action %r' % (
350                 view_modes, view_id, action))
351         action['views'] = [(False, mode) for mode in view_modes]
352         return
353     action['views'] = [(view_id, view_modes[0])]
354
355 def fix_view_modes(action):
356     """ For historical reasons, OpenERP has weird dealings in relation to
357     view_mode and the view_type attribute (on window actions):
358
359     * one of the view modes is ``tree``, which stands for both list views
360       and tree views
361     * the choice is made by checking ``view_type``, which is either
362       ``form`` for a list view or ``tree`` for an actual tree view
363
364     This methods simply folds the view_type into view_mode by adding a
365     new view mode ``list`` which is the result of the ``tree`` view_mode
366     in conjunction with the ``form`` view_type.
367
368     TODO: this should go into the doc, some kind of "peculiarities" section
369
370     :param dict action: an action descriptor
371     :returns: nothing, the action is modified in place
372     """
373     if 'views' not in action:
374         generate_views(action)
375
376     if action.pop('view_type') != 'form':
377         return action
378
379     action['views'] = [
380         [id, mode if mode != 'tree' else 'list']
381         for id, mode in action['views']
382     ]
383
384     return action
385
386 class Menu(openerpweb.Controller):
387     _cp_path = "/base/menu"
388
389     @openerpweb.jsonrequest
390     def load(self, req):
391         return {'data': self.do_load(req)}
392
393     def do_load(self, req):
394         """ Loads all menu items (all applications and their sub-menus).
395
396         :param req: A request object, with an OpenERP session attribute
397         :type req: < session -> OpenERPSession >
398         :return: the menu root
399         :rtype: dict('children': menu_nodes)
400         """
401         Menus = req.session.model('ir.ui.menu')
402         # menus are loaded fully unlike a regular tree view, cause there are
403         # less than 512 items
404         context = req.session.eval_context(req.context)
405         menu_ids = Menus.search([], 0, False, False, context)
406         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
407         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
408         menu_items.append(menu_root)
409
410         # make a tree using parent_id
411         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
412         for menu_item in menu_items:
413             if menu_item['parent_id']:
414                 parent = menu_item['parent_id'][0]
415             else:
416                 parent = False
417             if parent in menu_items_map:
418                 menu_items_map[parent].setdefault(
419                     'children', []).append(menu_item)
420
421         # sort by sequence a tree using parent_id
422         for menu_item in menu_items:
423             menu_item.setdefault('children', []).sort(
424                 key=lambda x:x["sequence"])
425
426         return menu_root
427
428     @openerpweb.jsonrequest
429     def action(self, req, menu_id):
430         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
431                                              [('ir.ui.menu', menu_id)], False,
432                                              req.session.eval_context(req.context))
433         return {"action": actions}
434
435 class DataSet(openerpweb.Controller):
436     _cp_path = "/base/dataset"
437
438     @openerpweb.jsonrequest
439     def fields(self, req, model):
440         return {'fields': req.session.model(model).fields_get(False,
441                                                               req.session.eval_context(req.context))}
442
443     @openerpweb.jsonrequest
444     def search_read(self, request, model, fields=False, offset=0, limit=False, domain=None, sort=None):
445         return self.do_search_read(request, model, fields, offset, limit, domain, sort)
446     def do_search_read(self, request, model, fields=False, offset=0, limit=False, domain=None
447                        , sort=None):
448         """ Performs a search() followed by a read() (if needed) using the
449         provided search criteria
450
451         :param request: a JSON-RPC request object
452         :type request: openerpweb.JsonRequest
453         :param str model: the name of the model to search on
454         :param fields: a list of the fields to return in the result records
455         :type fields: [str]
456         :param int offset: from which index should the results start being returned
457         :param int limit: the maximum number of records to return
458         :param list domain: the search domain for the query
459         :param list sort: sorting directives
460         :returns: A structure (dict) with two keys: ids (all the ids matching
461                   the (domain, context) pair) and records (paginated records
462                   matching fields selection set)
463         :rtype: list
464         """
465         Model = request.session.model(model)
466         context, domain = eval_context_and_domain(
467             request.session, request.context, domain)
468
469         ids = Model.search(domain, 0, False, sort or False, context)
470         # need to fill the dataset with all ids for the (domain, context) pair,
471         # so search un-paginated and paginate manually before reading
472         paginated_ids = ids[offset:(offset + limit if limit else None)]
473         if fields and fields == ['id']:
474             # shortcut read if we only want the ids
475             return {
476                 'ids': ids,
477                 'records': map(lambda id: {'id': id}, paginated_ids)
478             }
479
480         records = Model.read(paginated_ids, fields or False, context)
481         records.sort(key=lambda obj: ids.index(obj['id']))
482         return {
483             'ids': ids,
484             'records': records
485         }
486
487
488     @openerpweb.jsonrequest
489     def get(self, request, model, ids, fields=False):
490         return self.do_get(request, model, ids, fields)
491     def do_get(self, request, model, ids, fields=False):
492         """ Fetches and returns the records of the model ``model`` whose ids
493         are in ``ids``.
494
495         The results are in the same order as the inputs, but elements may be
496         missing (if there is no record left for the id)
497
498         :param request: the JSON-RPC2 request object
499         :type request: openerpweb.JsonRequest
500         :param model: the model to read from
501         :type model: str
502         :param ids: a list of identifiers
503         :type ids: list
504         :param fields: a list of fields to fetch, ``False`` or empty to fetch
505                        all fields in the model
506         :type fields: list | False
507         :returns: a list of records, in the same order as the list of ids
508         :rtype: list
509         """
510         Model = request.session.model(model)
511         records = Model.read(ids, fields, request.session.eval_context(request.context))
512
513         record_map = dict((record['id'], record) for record in records)
514
515         return [record_map[id] for id in ids if record_map.get(id)]
516
517     @openerpweb.jsonrequest
518     def load(self, req, model, id, fields):
519         m = req.session.model(model)
520         value = {}
521         r = m.read([id], False, req.session.eval_context(req.context))
522         if r:
523             value = r[0]
524         return {'value': value}
525
526     @openerpweb.jsonrequest
527     def create(self, req, model, data):
528         m = req.session.model(model)
529         r = m.create(data, req.session.eval_context(req.context))
530         return {'result': r}
531
532     @openerpweb.jsonrequest
533     def save(self, req, model, id, data):
534         m = req.session.model(model)
535         r = m.write([id], data, req.session.eval_context(req.context))
536         return {'result': r}
537
538     @openerpweb.jsonrequest
539     def unlink(self, request, model, ids=()):
540         Model = request.session.model(model)
541         return Model.unlink(ids, request.session.eval_context(request.context))
542
543     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
544         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
545         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
546         c, d = eval_context_and_domain(req.session, context, domain)
547         if domain_id and len(args) - 1 >= domain_id:
548             args[domain_id] = d
549         if context_id and len(args) - 1 >= context_id:
550             args[context_id] = c
551
552         return getattr(req.session.model(model), method)(*args)
553
554     @openerpweb.jsonrequest
555     def call(self, req, model, method, args, domain_id=None, context_id=None):
556         return self.call_common(req, model, method, args, domain_id, context_id)
557
558     @openerpweb.jsonrequest
559     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
560         action = self.call_common(req, model, method, args, domain_id, context_id)
561         if isinstance(action, dict) and action.get('type') != '':
562             return {'result': clean_action(action, req.session)}
563         return {'result': False}
564
565     @openerpweb.jsonrequest
566     def exec_workflow(self, req, model, id, signal):
567         r = req.session.exec_workflow(model, id, signal)
568         return {'result': r}
569
570     @openerpweb.jsonrequest
571     def default_get(self, req, model, fields):
572         Model = req.session.model(model)
573         return Model.default_get(fields, req.session.eval_context(req.context))
574
575 class DataGroup(openerpweb.Controller):
576     _cp_path = "/base/group"
577     @openerpweb.jsonrequest
578     def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
579         Model = request.session.model(model)
580         context, domain = eval_context_and_domain(request.session, request.context, domain)
581
582         return Model.read_group(
583             domain or [], fields, group_by_fields, 0, False,
584             dict(context, group_by=group_by_fields), sort or False)
585
586 class View(openerpweb.Controller):
587     _cp_path = "/base/view"
588
589     def fields_view_get(self, request, model, view_id, view_type,
590                         transform=True, toolbar=False, submenu=False):
591         Model = request.session.model(model)
592         context = request.session.eval_context(request.context)
593         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
594         # todo fme?: check that we should pass the evaluated context here
595         self.process_view(request.session, fvg, context, transform)
596         return fvg
597
598     def process_view(self, session, fvg, context, transform):
599         # depending on how it feels, xmlrpclib.ServerProxy can translate
600         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
601         # enjoy unicode strings which can not be trivially converted to
602         # strings, and it blows up during parsing.
603
604         # So ensure we fix this retardation by converting view xml back to
605         # bit strings.
606         if isinstance(fvg['arch'], unicode):
607             arch = fvg['arch'].encode('utf-8')
608         else:
609             arch = fvg['arch']
610
611         if transform:
612             evaluation_context = session.evaluation_context(context or {})
613             xml = self.transform_view(arch, session, evaluation_context)
614         else:
615             xml = ElementTree.fromstring(arch)
616         fvg['arch'] = Xml2Json.convert_element(xml)
617
618         for field in fvg['fields'].itervalues():
619             if field.get('views'):
620                 for view in field["views"].itervalues():
621                     self.process_view(session, view, None, transform)
622             if field.get('domain'):
623                 field["domain"] = self.parse_domain(field["domain"], session)
624             if field.get('context'):
625                 field["context"] = self.parse_context(field["context"], session)
626
627     @openerpweb.jsonrequest
628     def add_custom(self, request, view_id, arch):
629         CustomView = request.session.model('ir.ui.view.custom')
630         CustomView.create({
631             'user_id': request.session._uid,
632             'ref_id': view_id,
633             'arch': arch
634         }, request.session.eval_context(request.context))
635         return {'result': True}
636
637     @openerpweb.jsonrequest
638     def undo_custom(self, request, view_id, reset=False):
639         CustomView = request.session.model('ir.ui.view.custom')
640         context = request.session.eval_context(request.context)
641         vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
642                                     0, False, False, context)
643         if vcustom:
644             if reset:
645                 CustomView.unlink(vcustom, context)
646             else:
647                 CustomView.unlink([vcustom[0]], context)
648             return {'result': True}
649         return {'result': False}
650
651     def transform_view(self, view_string, session, context=None):
652         # transform nodes on the fly via iterparse, instead of
653         # doing it statically on the parsing result
654         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
655         root = None
656         for event, elem in parser:
657             if event == "start":
658                 if root is None:
659                     root = elem
660                 self.parse_domains_and_contexts(elem, session)
661         return root
662
663     def parse_domain(self, domain, session):
664         """ Parses an arbitrary string containing a domain, transforms it
665         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
666
667         :param domain: the domain to parse, if the domain is not a string it is assumed to
668         be a literal domain and is returned as-is
669         :param session: Current OpenERP session
670         :type session: openerpweb.openerpweb.OpenERPSession
671         """
672         if not isinstance(domain, (str, unicode)):
673             return domain
674         try:
675             return openerpweb.ast.literal_eval(domain)
676         except ValueError:
677             # not a literal
678             return openerpweb.nonliterals.Domain(session, domain)
679
680     def parse_context(self, context, session):
681         """ Parses an arbitrary string containing a context, transforms it
682         to either a literal context or a :class:`openerpweb.nonliterals.Context`
683
684         :param context: the context to parse, if the context is not a string it is assumed to
685         be a literal domain and is returned as-is
686         :param session: Current OpenERP session
687         :type session: openerpweb.openerpweb.OpenERPSession
688         """
689         if not isinstance(context, (str, unicode)):
690             return context
691         try:
692             return openerpweb.ast.literal_eval(context)
693         except ValueError:
694             return openerpweb.nonliterals.Context(session, context)
695
696     def parse_domains_and_contexts(self, elem, session):
697         """ Converts domains and contexts from the view into Python objects,
698         either literals if they can be parsed by literal_eval or a special
699         placeholder object if the domain or context refers to free variables.
700
701         :param elem: the current node being parsed
702         :type param: xml.etree.ElementTree.Element
703         :param session: OpenERP session object, used to store and retrieve
704                         non-literal objects
705         :type session: openerpweb.openerpweb.OpenERPSession
706         """
707         for el in ['domain', 'filter_domain']:
708             domain = elem.get(el, '').strip()
709             if domain:
710                 elem.set(el, self.parse_domain(domain, session))
711         for el in ['context', 'default_get']:
712             context_string = elem.get(el, '').strip()
713             if context_string:
714                 elem.set(el, self.parse_context(context_string, session))
715
716 class FormView(View):
717     _cp_path = "/base/formview"
718
719     @openerpweb.jsonrequest
720     def load(self, req, model, view_id, toolbar=False):
721         fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
722         return {'fields_view': fields_view}
723
724 class ListView(View):
725     _cp_path = "/base/listview"
726
727     @openerpweb.jsonrequest
728     def load(self, req, model, view_id, toolbar=False):
729         fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
730         return {'fields_view': fields_view}
731
732     def process_colors(self, view, row, context):
733         colors = view['arch']['attrs'].get('colors')
734
735         if not colors:
736             return None
737
738         color = [
739             pair.split(':')[0]
740             for pair in colors.split(';')
741             if eval(pair.split(':')[1], dict(context, **row))
742         ]
743
744         if not color:
745             return None
746         elif len(color) == 1:
747             return color[0]
748         return 'maroon'
749
750 class SearchView(View):
751     _cp_path = "/base/searchview"
752
753     @openerpweb.jsonrequest
754     def load(self, req, model, view_id):
755         fields_view = self.fields_view_get(req, model, view_id, 'search')
756         return {'fields_view': fields_view}
757
758     @openerpweb.jsonrequest
759     def fields_get(self, req, model):
760         Model = req.session.model(model)
761         fields = Model.fields_get(False, req.session.eval_context(req.context))
762         for field in fields.values():
763             # shouldn't convert the views too?
764             if field.get('domain'):
765                 field["domain"] = self.parse_domain(field["domain"], req.session)
766             if field.get('context'):
767                 field["context"] = self.parse_domain(field["context"], req.session)
768         return {'fields': fields}
769     
770     @openerpweb.jsonrequest
771     def get_filters(self, req, model):
772         Model = req.session.model("ir.filters")
773         filters = Model.get_filters(model)
774         for filter in filters:
775             filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
776             filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
777         return filters
778     
779     @openerpweb.jsonrequest
780     def save_filter(self, req, model, name, context_to_save, domain):
781         Model = req.session.model("ir.filters")
782         ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
783         ctx.session = req.session
784         ctx = ctx.evaluate()
785         domain = openerpweb.nonliterals.CompoundDomain(domain)
786         domain.session = req.session
787         domain = domain.evaluate()
788         uid = req.session._uid
789         context = req.session.eval_context(req.context)
790         to_return = Model.create_or_replace({"context": ctx,
791                                              "domain": domain,
792                                              "model_id": model,
793                                              "name": name,
794                                              "user_id": uid
795                                              }, context)
796         return to_return
797
798 class Binary(openerpweb.Controller):
799     _cp_path = "/base/binary"
800
801     @openerpweb.httprequest
802     def image(self, request, session_id, model, id, field, **kw):
803         cherrypy.response.headers['Content-Type'] = 'image/png'
804         Model = request.session.model(model)
805         context = request.session.eval_context(request.context)
806         try:
807             if not id:
808                 res = Model.default_get([field], context).get(field, '')
809             else:
810                 res = Model.read([int(id)], [field], context)[0].get(field, '')
811             return base64.decodestring(res)
812         except: # TODO: what's the exception here?
813             return self.placeholder()
814     def placeholder(self):
815         return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
816
817     @openerpweb.httprequest
818     def saveas(self, request, session_id, model, id, field, fieldname, **kw):
819         Model = request.session.model(model)
820         context = request.session.eval_context(request.context)
821         res = Model.read([int(id)], [field, fieldname], context)[0]
822         filecontent = res.get(field, '')
823         if not filecontent:
824             raise cherrypy.NotFound
825         else:
826             cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
827             filename = '%s_%s' % (model.replace('.', '_'), id)
828             if fieldname:
829                 filename = res.get(fieldname, '') or filename
830             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' +  filename
831             return base64.decodestring(filecontent)
832
833     @openerpweb.httprequest
834     def upload(self, request, session_id, callback, ufile=None):
835         cherrypy.response.timeout = 500
836         headers = {}
837         for key, val in cherrypy.request.headers.iteritems():
838             headers[key.lower()] = val
839         size = int(headers.get('content-length', 0))
840         # TODO: might be useful to have a configuration flag for max-length file uploads
841         try:
842             out = """<script language="javascript" type="text/javascript">
843                         var win = window.top.window,
844                             callback = win[%s];
845                         if (typeof(callback) === 'function') {
846                             callback.apply(this, %s);
847                         } else {
848                             win.jQuery('#oe_notification', win.document).notify('create', {
849                                 title: "Ajax File Upload",
850                                 text: "Could not find callback"
851                             });
852                         }
853                     </script>"""
854             data = ufile.file.read()
855             args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
856         except Exception, e:
857             args = [False, e.message]
858         return out % (simplejson.dumps(callback), simplejson.dumps(args))
859
860     @openerpweb.httprequest
861     def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
862         cherrypy.response.timeout = 500
863         context = request.session.eval_context(request.context)
864         Model = request.session.model('ir.attachment')
865         try:
866             out = """<script language="javascript" type="text/javascript">
867                         var win = window.top.window,
868                             callback = win[%s];
869                         if (typeof(callback) === 'function') {
870                             callback.call(this, %s);
871                         }
872                     </script>"""
873             attachment_id = Model.create({
874                 'name': ufile.filename,
875                 'datas': base64.encodestring(ufile.file.read()),
876                 'res_model': model,
877                 'res_id': int(id)
878             }, context)
879             args = {
880                 'filename': ufile.filename,
881                 'id':  attachment_id
882             }
883         except Exception, e:
884             args = { 'error': e.message }
885         return out % (simplejson.dumps(callback), simplejson.dumps(args))
886
887 class Action(openerpweb.Controller):
888     _cp_path = "/base/action"
889
890     @openerpweb.jsonrequest
891     def load(self, req, action_id):
892         Actions = req.session.model('ir.actions.actions')
893         value = False
894         context = req.session.eval_context(req.context)
895         action_type = Actions.read([action_id], ['type'], context)
896         if action_type:
897             action = req.session.model(action_type[0]['type']).read([action_id], False,
898                                                                     context)
899             if action:
900                 value = clean_action(action[0], req.session)
901         return {'result': value}
902
903     @openerpweb.jsonrequest
904     def run(self, req, action_id):
905         return clean_action(req.session.model('ir.actions.server').run(
906             [action_id], req.session.eval_context(req.context)), req.session)
907
908 def export_csv(fields, result):
909     import StringIO
910     fp = StringIO.StringIO()
911     writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
912
913     writer.writerow(fields)
914
915     for data in result:
916         row = []
917         for d in data:
918             if isinstance(d, basestring):
919                 d = d.replace('\n',' ').replace('\t',' ')
920                 try:
921                     d = d.encode('utf-8')
922                 except:
923                     pass
924             if d is False: d = None
925             row.append(d)
926         writer.writerow(row)
927
928     fp.seek(0)
929     data = fp.read()
930     fp.close()
931     return data
932
933 def export_xls(fieldnames, table):
934     import StringIO
935     try:
936         import xlwt
937     except ImportError:
938         common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
939
940     workbook = xlwt.Workbook()
941     worksheet = workbook.add_sheet('Sheet 1')
942
943     for i, fieldname in enumerate(fieldnames):
944         worksheet.write(0, i, str(fieldname))
945         worksheet.col(i).width = 8000 # around 220 pixels
946
947     style = xlwt.easyxf('align: wrap yes')
948
949     for row_index, row in enumerate(table):
950         for cell_index, cell_value in enumerate(row):
951             cell_value = str(cell_value)
952             cell_value = re.sub("\r", " ", cell_value)
953             worksheet.write(row_index + 1, cell_index, cell_value, style)
954
955
956     fp = StringIO.StringIO()
957     workbook.save(fp)
958     fp.seek(0)
959     data = fp.read()
960     fp.close()
961     #return data.decode('ISO-8859-1')
962     return unicode(data, 'utf-8', 'replace')
963
964 def node_attributes(node):
965     attrs = node.attributes
966
967     if not attrs:
968         return {}
969     # localName can be a unicode string, we're using attribute names as
970     # **kwargs keys and python-level kwargs don't take unicode keys kindly
971     # (they blow up) so we need to ensure all keys are ``str``
972     return dict([(str(attrs.item(i).localName), attrs.item(i).nodeValue)
973                  for i in range(attrs.length)])
974
975 def _fields_get_all(req, model, views, context=None):
976
977     if context is None:
978         context = {}
979
980     def parse(root, fields):
981         for node in root.childNodes:
982             if node.nodeName in ('form', 'notebook', 'page', 'group', 'tree', 'hpaned', 'vpaned'):
983                 parse(node, fields)
984             elif node.nodeName=='field':
985                 attrs = node_attributes(node)
986                 name = attrs['name']
987                 fields[name].update(attrs)
988         return fields
989
990     def get_view_fields(view):
991         return parse(
992             xml.dom.minidom.parseString(view['arch'].encode('utf-8')).documentElement,
993             view['fields'])
994
995     model_obj = req.session.model(model)
996     tree_view = model_obj.fields_view_get(views.get('tree', False), 'tree', context)
997     form_view = model_obj.fields_view_get(views.get('form', False), 'form', context)
998     fields = {}
999     fields.update(get_view_fields(tree_view))
1000     fields.update(get_view_fields(form_view))
1001     return fields
1002
1003
1004 class Export(View):
1005     _cp_path = "/base/export"
1006
1007     def fields_get(self, req, model):
1008         Model = req.session.model(model)
1009         fields = Model.fields_get(False, req.session.eval_context(req.context))
1010         return fields
1011
1012     @openerpweb.jsonrequest
1013     def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1014         import_compat = params.get("import_compat", False)
1015         views_id = params.get("views_id", {})
1016
1017         fields = _fields_get_all(req, model, views=views_id, context=req.session.eval_context(req.context))
1018         field_parent_type = params.get("parent_field_type",False)
1019
1020         if import_compat and field_parent_type and field_parent_type == "many2one":
1021             fields = {}
1022
1023         fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1024         records = []
1025         fields_order = fields.keys()
1026         fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1027
1028         for index, field in enumerate(fields_order):
1029             value = fields[field]
1030             record = {}
1031             if import_compat and value.get('readonly', False):
1032                 ok = False
1033                 for sl in value.get('states', {}).values():
1034                     for s in sl:
1035                         ok = ok or (s==['readonly',False])
1036                 if not ok: continue
1037
1038             id = prefix + (prefix and '/'or '') + field
1039             nm = name + (name and '/' or '') + value['string']
1040             record.update(id=id, string= nm, action='javascript: void(0)',
1041                           target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1042             records.append(record)
1043
1044             if len(nm.split('/')) < 3 and value.get('relation', False):
1045                 if import_compat:
1046                     ref = value.pop('relation')
1047                     cfields = self.fields_get(req, ref)
1048                     if (value['type'] == 'many2many'):
1049                         record['children'] = []
1050                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1051
1052                     elif value['type'] == 'many2one':
1053                         record['children'] = [id + '/id', id + '/.id']
1054                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1055
1056                     else:
1057                         cfields_order = cfields.keys()
1058                         cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1059                         children = []
1060                         for j, fld in enumerate(cfields_order):
1061                             cid = id + '/' + fld
1062                             cid = cid.replace(' ', '_')
1063                             children.append(cid)
1064                         record['children'] = children or []
1065                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1066                 else:
1067                     ref = value.pop('relation')
1068                     cfields = self.fields_get(req, ref)
1069                     cfields_order = cfields.keys()
1070                     cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1071                     children = []
1072                     for j, fld in enumerate(cfields_order):
1073                         cid = id + '/' + fld
1074                         cid = cid.replace(' ', '_')
1075                         children.append(cid)
1076                     record['children'] = children or []
1077                     record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1078
1079         records.reverse()
1080         return records
1081
1082     @openerpweb.jsonrequest
1083     def save_export_lists(self, req, name, model, field_list):
1084         result = {'resource':model, 'name':name, 'export_fields': []}
1085         for field in field_list:
1086             result['export_fields'].append((0, 0, {'name': field}))
1087         return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1088
1089     @openerpweb.jsonrequest
1090     def exist_export_lists(self, req, model):
1091         export_model = req.session.model("ir.exports")
1092         return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1093
1094     @openerpweb.jsonrequest
1095     def delete_export(self, req, export_id):
1096         req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1097         return True
1098
1099     @openerpweb.jsonrequest
1100     def namelist(self,req,  model, export_id):
1101
1102         result = self.get_data(req, model, req.session.eval_context(req.context))
1103         ir_export_obj = req.session.model("ir.exports")
1104         ir_export_line_obj = req.session.model("ir.exports.line")
1105
1106         field = ir_export_obj.read(export_id)
1107         fields = ir_export_line_obj.read(field['export_fields'])
1108
1109         name_list = {}
1110         [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1111         return name_list
1112
1113     def get_data(self, req, model, context=None):
1114         ids = []
1115         context = context or {}
1116         fields_data = {}
1117         proxy = req.session.model(model)
1118         fields = self.fields_get(req, model)
1119         if not ids:
1120             f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1121             f2 = proxy.fields_view_get(False, 'form', context)['fields']
1122
1123             fields = dict(f1)
1124             fields.update(f2)
1125             fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1126
1127         def rec(fields):
1128             _fields = {'id': 'ID' , '.id': 'Database ID' }
1129             def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1130                 fields_order = fields.keys()
1131                 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1132
1133                 for field in fields_order:
1134                     fields_data[prefix_node+field] = fields[field]
1135                     if prefix_node:
1136                         fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1137                     st_name = fields[field]['string'] or field
1138                     _fields[prefix_node+field] = st_name
1139                     if fields[field].get('relation', False) and level>0:
1140                         fields2 = self.fields_get(req,  fields[field]['relation'])
1141                         fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1142                         model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1143             model_populate(fields)
1144             return _fields
1145         return rec(fields)
1146
1147     @openerpweb.jsonrequest
1148     def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1149         context = req.session.eval_context(req.context)
1150         modle_obj = req.session.model(model)
1151         ids = ids or modle_obj.search(domain, context=context)
1152
1153         field = fields.keys()
1154         result = modle_obj.export_data(ids, field , context).get('datas',[])
1155
1156         if not import_compat:
1157             field = [val.strip() for val in fields.values()]
1158
1159         if export_format == 'xls':
1160             return export_xls(field, result)
1161         else:
1162             return export_csv(field, result)
1163