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