[ADD] handling of binary fields to the listview
[odoo/odoo.git] / addons / web / controllers / main.py
1 # -*- coding: utf-8 -*-
2
3 import ast
4 import base64
5 import csv
6 import glob
7 import itertools
8 import operator
9 import os
10 import re
11 import simplejson
12 import time
13 import urllib2
14 import xmlrpclib
15 import zlib
16 from xml.etree import ElementTree
17 from cStringIO import StringIO
18
19 import babel.messages.pofile
20 import werkzeug.utils
21
22 import web.common
23 openerpweb = web.common.http
24
25 #----------------------------------------------------------
26 # OpenERP Web web Controllers
27 #----------------------------------------------------------
28
29
30 def concat_xml(file_list):
31     """Concatenate xml files
32     return (concat,timestamp)
33     concat: concatenation of file content
34     timestamp: max(os.path.getmtime of file_list)
35     """
36     root = None
37     files_timestamp = 0
38     for fname in file_list:
39         ftime = os.path.getmtime(fname)
40         if ftime > files_timestamp:
41             files_timestamp = ftime
42
43         xml = ElementTree.parse(fname).getroot()
44
45         if root is None:
46             root = ElementTree.Element(xml.tag)
47         #elif root.tag != xml.tag:
48         #    raise ValueError("Root tags missmatch: %r != %r" % (root.tag, xml.tag))
49
50         for child in xml.getchildren():
51             root.append(child)
52     return ElementTree.tostring(root, 'utf-8'), files_timestamp
53
54
55 def concat_files(file_list, reader=None):
56     """ Concatenate file content
57     return (concat,timestamp)
58     concat: concatenation of file content, read by `reader`
59     timestamp: max(os.path.getmtime of file_list)
60     """
61     if reader is None:
62         def reader(f):
63             with open(f) as fp:
64                 return fp.read()
65
66     files_content = []
67     files_timestamp = 0
68     for fname in file_list:
69         ftime = os.path.getmtime(fname)
70         if ftime > files_timestamp:
71             files_timestamp = ftime
72
73         files_content.append(reader(fname))
74     files_concat = "".join(files_content)
75     return files_concat,files_timestamp
76
77 html_template = """<!DOCTYPE html>
78 <html style="height: 100%%">
79     <head>
80         <meta http-equiv="content-type" content="text/html; charset=utf-8" />
81         <title>OpenERP</title>
82         <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
83         %(css)s
84         %(js)s
85         <script type="text/javascript">
86             $(function() {
87                 var s = new openerp.init(%(modules)s);
88                 %(init)s
89             });
90         </script>
91     </head>
92     <body></body>
93 </html>
94 """
95
96 class WebClient(openerpweb.Controller):
97     _cp_path = "/web/webclient"
98
99     def server_wide_modules(self, req):
100         addons = [i for i in req.config.server_wide_modules if i in openerpweb.addons_manifest]
101         return addons
102
103     def manifest_glob(self, req, addons, key):
104         if addons is None:
105             addons = self.server_wide_modules(req)
106         else:
107             addons = addons.split(',')
108         r = []
109         for addon in addons:
110             manifest = openerpweb.addons_manifest.get(addon, None)
111             if not manifest:
112                 continue
113             # ensure does not ends with /
114             addons_path = os.path.join(manifest['addons_path'], '')[:-1]
115             globlist = manifest.get(key, [])
116             for pattern in globlist:
117                 for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))):
118                     r.append( (path, path[len(addons_path):]))
119         return r
120
121     def manifest_list(self, req, mods, extension):
122         if not req.debug:
123             path = '/web/webclient/' + extension
124             if mods is not None:
125                 path += '?mods=' + mods
126             return [path]
127         return ['%s?debug=%s' % (wp, os.path.getmtime(fp)) for fp, wp in self.manifest_glob(req, mods, extension)]
128
129     @openerpweb.jsonrequest
130     def csslist(self, req, mods=None):
131         return self.manifest_list(req, mods, 'css')
132
133     @openerpweb.jsonrequest
134     def jslist(self, req, mods=None):
135         return self.manifest_list(req, mods, 'js')
136
137     @openerpweb.jsonrequest
138     def qweblist(self, req, mods=None):
139         return self.manifest_list(req, mods, 'qweb')
140
141     @openerpweb.httprequest
142     def css(self, req, mods=None):
143
144         files = list(self.manifest_glob(req, mods, 'css'))
145         file_map = dict(files)
146
147         rx_import = re.compile(r"""@import\s+('|")(?!'|"|/|https?://)""", re.U)
148         rx_url = re.compile(r"""url\s*\(\s*('|"|)(?!'|"|/|https?://)""", re.U)
149
150
151         def reader(f):
152             """read the a css file and absolutify all relative uris"""
153             with open(f) as fp:
154                 data = fp.read()
155
156             web_path = file_map[f]
157             web_dir = os.path.dirname(web_path)
158
159             data = re.sub(
160                 rx_import,
161                 r"""@import \1%s/""" % (web_dir,),
162                 data,
163             )
164
165             data = re.sub(
166                 rx_url,
167                 r"""url(\1%s/""" % (web_dir,),
168                 data,
169             )
170             return data
171
172         content,timestamp = concat_files((f[0] for f in files), reader)
173         # TODO use timestamp to set Last mofified date and E-tag
174         return req.make_response(content, [('Content-Type', 'text/css')])
175
176     @openerpweb.httprequest
177     def js(self, req, mods=None):
178         files = [f[0] for f in self.manifest_glob(req, mods, 'js')]
179         content,timestamp = concat_files(files)
180         # TODO use timestamp to set Last mofified date and E-tag
181         return req.make_response(content, [('Content-Type', 'application/javascript')])
182
183     @openerpweb.httprequest
184     def qweb(self, req, mods=None):
185         files = [f[0] for f in self.manifest_glob(req, mods, 'qweb')]
186         content,timestamp = concat_xml(files)
187         # TODO use timestamp to set Last mofified date and E-tag
188         return req.make_response(content, [('Content-Type', 'text/xml')])
189
190
191     @openerpweb.httprequest
192     def home(self, req, s_action=None, **kw):
193         js = "\n        ".join('<script type="text/javascript" src="%s"></script>'%i for i in self.manifest_list(req, None, 'js'))
194         css = "\n        ".join('<link rel="stylesheet" href="%s">'%i for i in self.manifest_list(req, None, 'css'))
195
196         r = html_template % {
197             'js': js,
198             'css': css,
199             'modules': simplejson.dumps(self.server_wide_modules(req)),
200             'init': 'new s.web.WebClient().replace($("body"));',
201         }
202         return r
203
204     @openerpweb.httprequest
205     def login(self, req, db, login, key):
206         req.session.authenticate(db, login, key, {})
207         redirect = werkzeug.utils.redirect('/web/webclient/home', 303)
208         cookie_val = urllib2.quote(simplejson.dumps(req.session_id))
209         redirect.set_cookie('session0|session_id', cookie_val)
210         return redirect
211
212     @openerpweb.jsonrequest
213     def translations(self, req, mods, lang):
214         lang_model = req.session.model('res.lang')
215         ids = lang_model.search([("code", "=", lang)])
216         if ids:
217             lang_obj = lang_model.read(ids[0], ["direction", "date_format", "time_format",
218                                                 "grouping", "decimal_point", "thousands_sep"])
219         else:
220             lang_obj = None
221
222         if lang.count("_") > 0:
223             separator = "_"
224         else:
225             separator = "@"
226         langs = lang.split(separator)
227         langs = [separator.join(langs[:x]) for x in range(1, len(langs) + 1)]
228
229         transs = {}
230         for addon_name in mods:
231             transl = {"messages":[]}
232             transs[addon_name] = transl
233             for l in langs:
234                 addons_path = openerpweb.addons_manifest[addon_name]['addons_path']
235                 f_name = os.path.join(addons_path, addon_name, "po", l + ".po")
236                 if not os.path.exists(f_name):
237                     continue
238                 try:
239                     with open(f_name) as t_file:
240                         po = babel.messages.pofile.read_po(t_file)
241                 except:
242                     continue
243                 for x in po:
244                     if x.id and x.string:
245                         transl["messages"].append({'id': x.id, 'string': x.string})
246         return {"modules": transs,
247                 "lang_parameters": lang_obj}
248
249     @openerpweb.jsonrequest
250     def version_info(self, req):
251         return {
252             "version": web.common.release.version
253         }
254
255 class Proxy(openerpweb.Controller):
256     _cp_path = '/web/proxy'
257
258     @openerpweb.jsonrequest
259     def load(self, req, path):
260         """ Proxies an HTTP request through a JSON request.
261
262         It is strongly recommended to not request binary files through this,
263         as the result will be a binary data blob as well.
264
265         :param req: OpenERP request
266         :param path: actual request path
267         :return: file content
268         """
269         from werkzeug.test import Client
270         from werkzeug.wrappers import BaseResponse
271
272         return Client(req.httprequest.app, BaseResponse).get(path).data
273
274 class Database(openerpweb.Controller):
275     _cp_path = "/web/database"
276
277     @openerpweb.jsonrequest
278     def get_list(self, req):
279         proxy = req.session.proxy("db")
280         dbs = proxy.list()
281         h = req.httprequest.headers['Host'].split(':')[0]
282         d = h.split('.')[0]
283         r = req.config.dbfilter.replace('%h', h).replace('%d', d)
284         dbs = [i for i in dbs if re.match(r, i)]
285         return {"db_list": dbs}
286
287     @openerpweb.jsonrequest
288     def progress(self, req, password, id):
289         return req.session.proxy('db').get_progress(password, id)
290
291     @openerpweb.jsonrequest
292     def create(self, req, fields):
293
294         params = dict(map(operator.itemgetter('name', 'value'), fields))
295         create_attrs = (
296             params['super_admin_pwd'],
297             params['db_name'],
298             bool(params.get('demo_data')),
299             params['db_lang'],
300             params['create_admin_pwd']
301         )
302
303         try:
304             return req.session.proxy("db").create(*create_attrs)
305         except xmlrpclib.Fault, e:
306             if e.faultCode and isinstance(e.faultCode, str)\
307                 and e.faultCode.split(':')[0] == 'AccessDenied':
308                     return {'error': e.faultCode, 'title': 'Database creation error'}
309             return {
310                 'error': "Could not create database '%s': %s" % (
311                     params['db_name'], e.faultString),
312                 'title': 'Database creation error'
313             }
314
315     @openerpweb.jsonrequest
316     def drop(self, req, fields):
317         password, db = operator.itemgetter(
318             'drop_pwd', 'drop_db')(
319                 dict(map(operator.itemgetter('name', 'value'), fields)))
320
321         try:
322             return req.session.proxy("db").drop(password, db)
323         except xmlrpclib.Fault, e:
324             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
325                 return {'error': e.faultCode, 'title': 'Drop Database'}
326         return {'error': 'Could not drop database !', 'title': 'Drop Database'}
327
328     @openerpweb.httprequest
329     def backup(self, req, backup_db, backup_pwd, token):
330         try:
331             db_dump = base64.b64decode(
332                 req.session.proxy("db").dump(backup_pwd, backup_db))
333             return req.make_response(db_dump,
334                 [('Content-Type', 'application/octet-stream; charset=binary'),
335                  ('Content-Disposition', 'attachment; filename="' + backup_db + '.dump"')],
336                 {'fileToken': int(token)}
337             )
338         except xmlrpclib.Fault, e:
339             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
340                 return 'Backup Database|' + e.faultCode
341         return 'Backup Database|Could not generate database backup'
342
343     @openerpweb.httprequest
344     def restore(self, req, db_file, restore_pwd, new_db):
345         try:
346             data = base64.b64encode(db_file.read())
347             req.session.proxy("db").restore(restore_pwd, new_db, data)
348             return ''
349         except xmlrpclib.Fault, e:
350             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
351                 raise Exception("AccessDenied")
352
353     @openerpweb.jsonrequest
354     def change_password(self, req, fields):
355         old_password, new_password = operator.itemgetter(
356             'old_pwd', 'new_pwd')(
357                 dict(map(operator.itemgetter('name', 'value'), fields)))
358         try:
359             return req.session.proxy("db").change_admin_password(old_password, new_password)
360         except xmlrpclib.Fault, e:
361             if e.faultCode and e.faultCode.split(':')[0] == 'AccessDenied':
362                 return {'error': e.faultCode, 'title': 'Change Password'}
363         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
364
365 class Session(openerpweb.Controller):
366     _cp_path = "/web/session"
367
368     def session_info(self, req):
369         return {
370             "session_id": req.session_id,
371             "uid": req.session._uid,
372             "context": req.session.get_context() if req.session._uid else {},
373             "db": req.session._db,
374             "login": req.session._login,
375             "openerp_entreprise": req.session.openerp_entreprise(),
376         }
377
378     @openerpweb.jsonrequest
379     def get_session_info(self, req):
380         return self.session_info(req)
381
382     @openerpweb.jsonrequest
383     def authenticate(self, req, db, login, password, base_location=None):
384         wsgienv = req.httprequest.environ
385         release = web.common.release
386         env = dict(
387             base_location=base_location,
388             HTTP_HOST=wsgienv['HTTP_HOST'],
389             REMOTE_ADDR=wsgienv['REMOTE_ADDR'],
390             user_agent="%s / %s" % (release.name, release.version),
391         )
392         req.session.authenticate(db, login, password, env)
393
394         return self.session_info(req)
395
396     @openerpweb.jsonrequest
397     def change_password (self,req,fields):
398         old_password, new_password,confirm_password = operator.itemgetter('old_pwd', 'new_password','confirm_pwd')(
399                 dict(map(operator.itemgetter('name', 'value'), fields)))
400         if not (old_password.strip() and new_password.strip() and confirm_password.strip()):
401             return {'error':'All passwords have to be filled.','title': 'Change Password'}
402         if new_password != confirm_password:
403             return {'error': 'The new password and its confirmation must be identical.','title': 'Change Password'}
404         try:
405             if req.session.model('res.users').change_password(
406                 old_password, new_password):
407                 return {'new_password':new_password}
408         except:
409             return {'error': 'Original password incorrect, your password was not changed.', 'title': 'Change Password'}
410         return {'error': 'Error, password not changed !', 'title': 'Change Password'}
411
412     @openerpweb.jsonrequest
413     def sc_list(self, req):
414         return req.session.model('ir.ui.view_sc').get_sc(
415             req.session._uid, "ir.ui.menu", req.session.eval_context(req.context))
416
417     @openerpweb.jsonrequest
418     def get_lang_list(self, req):
419         try:
420             return {
421                 'lang_list': (req.session.proxy("db").list_lang() or []),
422                 'error': ""
423             }
424         except Exception, e:
425             return {"error": e, "title": "Languages"}
426
427     @openerpweb.jsonrequest
428     def modules(self, req):
429         # Compute available candidates module
430         loadable = openerpweb.addons_manifest.iterkeys()
431         loaded = req.config.server_wide_modules
432         candidates = [mod for mod in loadable if mod not in loaded]
433
434         # Compute active true modules that might be on the web side only
435         active = set(name for name in candidates
436                      if openerpweb.addons_manifest[name].get('active'))
437
438         # Retrieve database installed modules
439         Modules = req.session.model('ir.module.module')
440         installed = set(module['name'] for module in Modules.search_read(
441             [('state','=','installed'), ('name','in', candidates)], ['name']))
442
443         # Merge both
444         return list(active | installed)
445
446     @openerpweb.jsonrequest
447     def eval_domain_and_context(self, req, contexts, domains,
448                                 group_by_seq=None):
449         """ Evaluates sequences of domains and contexts, composing them into
450         a single context, domain or group_by sequence.
451
452         :param list contexts: list of contexts to merge together. Contexts are
453                               evaluated in sequence, all previous contexts
454                               are part of their own evaluation context
455                               (starting at the session context).
456         :param list domains: list of domains to merge together. Domains are
457                              evaluated in sequence and appended to one another
458                              (implicit AND), their evaluation domain is the
459                              result of merging all contexts.
460         :param list group_by_seq: list of domains (which may be in a different
461                                   order than the ``contexts`` parameter),
462                                   evaluated in sequence, their ``'group_by'``
463                                   key is extracted if they have one.
464         :returns:
465             a 3-dict of:
466
467             context (``dict``)
468                 the global context created by merging all of
469                 ``contexts``
470
471             domain (``list``)
472                 the concatenation of all domains
473
474             group_by (``list``)
475                 a list of fields to group by, potentially empty (in which case
476                 no group by should be performed)
477         """
478         context, domain = eval_context_and_domain(req.session,
479                                                   web.common.nonliterals.CompoundContext(*(contexts or [])),
480                                                   web.common.nonliterals.CompoundDomain(*(domains or [])))
481
482         group_by_sequence = []
483         for candidate in (group_by_seq or []):
484             ctx = req.session.eval_context(candidate, context)
485             group_by = ctx.get('group_by')
486             if not group_by:
487                 continue
488             elif isinstance(group_by, basestring):
489                 group_by_sequence.append(group_by)
490             else:
491                 group_by_sequence.extend(group_by)
492
493         return {
494             'context': context,
495             'domain': domain,
496             'group_by': group_by_sequence
497         }
498
499     @openerpweb.jsonrequest
500     def save_session_action(self, req, the_action):
501         """
502         This method store an action object in the session object and returns an integer
503         identifying that action. The method get_session_action() can be used to get
504         back the action.
505
506         :param the_action: The action to save in the session.
507         :type the_action: anything
508         :return: A key identifying the saved action.
509         :rtype: integer
510         """
511         saved_actions = req.httpsession.get('saved_actions')
512         if not saved_actions:
513             saved_actions = {"next":0, "actions":{}}
514             req.httpsession['saved_actions'] = saved_actions
515         # we don't allow more than 10 stored actions
516         if len(saved_actions["actions"]) >= 10:
517             del saved_actions["actions"][min(saved_actions["actions"].keys())]
518         key = saved_actions["next"]
519         saved_actions["actions"][key] = the_action
520         saved_actions["next"] = key + 1
521         return key
522
523     @openerpweb.jsonrequest
524     def get_session_action(self, req, key):
525         """
526         Gets back a previously saved action. This method can return None if the action
527         was saved since too much time (this case should be handled in a smart way).
528
529         :param key: The key given by save_session_action()
530         :type key: integer
531         :return: The saved action or None.
532         :rtype: anything
533         """
534         saved_actions = req.httpsession.get('saved_actions')
535         if not saved_actions:
536             return None
537         return saved_actions["actions"].get(key)
538
539     @openerpweb.jsonrequest
540     def check(self, req):
541         req.session.assert_valid()
542         return None
543
544 def eval_context_and_domain(session, context, domain=None):
545     e_context = session.eval_context(context)
546     # should we give the evaluated context as an evaluation context to the domain?
547     e_domain = session.eval_domain(domain or [])
548
549     return e_context, e_domain
550
551 def load_actions_from_ir_values(req, key, key2, models, meta):
552     context = req.session.eval_context(req.context)
553     Values = req.session.model('ir.values')
554     actions = Values.get(key, key2, models, meta, context)
555
556     return [(id, name, clean_action(req, action))
557             for id, name, action in actions]
558
559 def clean_action(req, action, do_not_eval=False):
560     action.setdefault('flags', {})
561
562     context = req.session.eval_context(req.context)
563     eval_ctx = req.session.evaluation_context(context)
564
565     if not do_not_eval:
566         # values come from the server, we can just eval them
567         if isinstance(action.get('context'), basestring):
568             action['context'] = eval( action['context'], eval_ctx ) or {}
569
570         if isinstance(action.get('domain'), basestring):
571             action['domain'] = eval( action['domain'], eval_ctx ) or []
572     else:
573         if 'context' in action:
574             action['context'] = parse_context(action['context'], req.session)
575         if 'domain' in action:
576             action['domain'] = parse_domain(action['domain'], req.session)
577
578     if 'type' not in action:
579        action['type'] = 'ir.actions.act_window_close'
580
581     if action['type'] == 'ir.actions.act_window':
582         return fix_view_modes(action)
583     return action
584
585 # I think generate_views,fix_view_modes should go into js ActionManager
586 def generate_views(action):
587     """
588     While the server generates a sequence called "views" computing dependencies
589     between a bunch of stuff for views coming directly from the database
590     (the ``ir.actions.act_window model``), it's also possible for e.g. buttons
591     to return custom view dictionaries generated on the fly.
592
593     In that case, there is no ``views`` key available on the action.
594
595     Since the web client relies on ``action['views']``, generate it here from
596     ``view_mode`` and ``view_id``.
597
598     Currently handles two different cases:
599
600     * no view_id, multiple view_mode
601     * single view_id, single view_mode
602
603     :param dict action: action descriptor dictionary to generate a views key for
604     """
605     view_id = action.get('view_id', False)
606     if isinstance(view_id, (list, tuple)):
607         view_id = view_id[0]
608
609     # providing at least one view mode is a requirement, not an option
610     view_modes = action['view_mode'].split(',')
611
612     if len(view_modes) > 1:
613         if view_id:
614             raise ValueError('Non-db action dictionaries should provide '
615                              'either multiple view modes or a single view '
616                              'mode and an optional view id.\n\n Got view '
617                              'modes %r and view id %r for action %r' % (
618                 view_modes, view_id, action))
619         action['views'] = [(False, mode) for mode in view_modes]
620         return
621     action['views'] = [(view_id, view_modes[0])]
622
623 def fix_view_modes(action):
624     """ For historical reasons, OpenERP has weird dealings in relation to
625     view_mode and the view_type attribute (on window actions):
626
627     * one of the view modes is ``tree``, which stands for both list views
628       and tree views
629     * the choice is made by checking ``view_type``, which is either
630       ``form`` for a list view or ``tree`` for an actual tree view
631
632     This methods simply folds the view_type into view_mode by adding a
633     new view mode ``list`` which is the result of the ``tree`` view_mode
634     in conjunction with the ``form`` view_type.
635
636     TODO: this should go into the doc, some kind of "peculiarities" section
637
638     :param dict action: an action descriptor
639     :returns: nothing, the action is modified in place
640     """
641     if not action.get('views'):
642         generate_views(action)
643
644     id_form = None
645     for index, (id, mode) in enumerate(action['views']):
646         if mode == 'form':
647             id_form = id
648             break
649     if id_form is not None:
650         action['views'].insert(index + 1, (id_form, 'page'))
651
652     if action.pop('view_type', 'form') != 'form':
653         return action
654
655     action['views'] = [
656         [id, mode if mode != 'tree' else 'list']
657         for id, mode in action['views']
658     ]
659
660     return action
661
662 class Menu(openerpweb.Controller):
663     _cp_path = "/web/menu"
664
665     @openerpweb.jsonrequest
666     def load(self, req):
667         return {'data': self.do_load(req)}
668
669     def do_load(self, req):
670         """ Loads all menu items (all applications and their sub-menus).
671
672         :param req: A request object, with an OpenERP session attribute
673         :type req: < session -> OpenERPSession >
674         :return: the menu root
675         :rtype: dict('children': menu_nodes)
676         """
677         Menus = req.session.model('ir.ui.menu')
678         # menus are loaded fully unlike a regular tree view, cause there are
679         # less than 512 items
680         context = req.session.eval_context(req.context)
681         menu_ids = Menus.search([], 0, False, False, context)
682         menu_items = Menus.read(menu_ids, ['name', 'sequence', 'parent_id'], context)
683         menu_root = {'id': False, 'name': 'root', 'parent_id': [-1, '']}
684         menu_items.append(menu_root)
685
686         # make a tree using parent_id
687         menu_items_map = dict((menu_item["id"], menu_item) for menu_item in menu_items)
688         for menu_item in menu_items:
689             if menu_item['parent_id']:
690                 parent = menu_item['parent_id'][0]
691             else:
692                 parent = False
693             if parent in menu_items_map:
694                 menu_items_map[parent].setdefault(
695                     'children', []).append(menu_item)
696
697         # sort by sequence a tree using parent_id
698         for menu_item in menu_items:
699             menu_item.setdefault('children', []).sort(
700                 key=lambda x:x["sequence"])
701
702         return menu_root
703
704     @openerpweb.jsonrequest
705     def action(self, req, menu_id):
706         actions = load_actions_from_ir_values(req,'action', 'tree_but_open',
707                                              [('ir.ui.menu', menu_id)], False)
708         return {"action": actions}
709
710 class DataSet(openerpweb.Controller):
711     _cp_path = "/web/dataset"
712
713     @openerpweb.jsonrequest
714     def fields(self, req, model):
715         return {'fields': req.session.model(model).fields_get(False,
716                                                               req.session.eval_context(req.context))}
717
718     @openerpweb.jsonrequest
719     def search_read(self, req, model, fields=False, offset=0, limit=False, domain=None, sort=None):
720         return self.do_search_read(req, model, fields, offset, limit, domain, sort)
721     def do_search_read(self, req, model, fields=False, offset=0, limit=False, domain=None
722                        , sort=None):
723         """ Performs a search() followed by a read() (if needed) using the
724         provided search criteria
725
726         :param req: a JSON-RPC request object
727         :type req: openerpweb.JsonRequest
728         :param str model: the name of the model to search on
729         :param fields: a list of the fields to return in the result records
730         :type fields: [str]
731         :param int offset: from which index should the results start being returned
732         :param int limit: the maximum number of records to return
733         :param list domain: the search domain for the query
734         :param list sort: sorting directives
735         :returns: A structure (dict) with two keys: ids (all the ids matching
736                   the (domain, context) pair) and records (paginated records
737                   matching fields selection set)
738         :rtype: list
739         """
740         Model = req.session.model(model)
741
742         context, domain = eval_context_and_domain(
743             req.session, req.context, domain)
744
745         ids = Model.search(domain, 0, False, sort or False, context)
746         # need to fill the dataset with all ids for the (domain, context) pair,
747         # so search un-paginated and paginate manually before reading
748         paginated_ids = ids[offset:(offset + limit if limit else None)]
749         if fields and fields == ['id']:
750             # shortcut read if we only want the ids
751             return {
752                 'ids': ids,
753                 'records': map(lambda id: {'id': id}, paginated_ids)
754             }
755
756         records = Model.read(paginated_ids, fields or False, context)
757         records.sort(key=lambda obj: ids.index(obj['id']))
758         return {
759             'ids': ids,
760             'records': records
761         }
762
763
764     @openerpweb.jsonrequest
765     def read(self, req, model, ids, fields=False):
766         return self.do_search_read(req, model, ids, fields)
767
768     @openerpweb.jsonrequest
769     def get(self, req, model, ids, fields=False):
770         return self.do_get(req, model, ids, fields)
771
772     def do_get(self, req, model, ids, fields=False):
773         """ Fetches and returns the records of the model ``model`` whose ids
774         are in ``ids``.
775
776         The results are in the same order as the inputs, but elements may be
777         missing (if there is no record left for the id)
778
779         :param req: the JSON-RPC2 request object
780         :type req: openerpweb.JsonRequest
781         :param model: the model to read from
782         :type model: str
783         :param ids: a list of identifiers
784         :type ids: list
785         :param fields: a list of fields to fetch, ``False`` or empty to fetch
786                        all fields in the model
787         :type fields: list | False
788         :returns: a list of records, in the same order as the list of ids
789         :rtype: list
790         """
791         Model = req.session.model(model)
792         records = Model.read(ids, fields, req.session.eval_context(req.context))
793
794         record_map = dict((record['id'], record) for record in records)
795
796         return [record_map[id] for id in ids if record_map.get(id)]
797
798     @openerpweb.jsonrequest
799     def load(self, req, model, id, fields):
800         m = req.session.model(model)
801         value = {}
802         r = m.read([id], False, req.session.eval_context(req.context))
803         if r:
804             value = r[0]
805         return {'value': value}
806
807     @openerpweb.jsonrequest
808     def create(self, req, model, data):
809         m = req.session.model(model)
810         r = m.create(data, req.session.eval_context(req.context))
811         return {'result': r}
812
813     @openerpweb.jsonrequest
814     def save(self, req, model, id, data):
815         m = req.session.model(model)
816         r = m.write([id], data, req.session.eval_context(req.context))
817         return {'result': r}
818
819     @openerpweb.jsonrequest
820     def unlink(self, req, model, ids=()):
821         Model = req.session.model(model)
822         return Model.unlink(ids, req.session.eval_context(req.context))
823
824     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
825         has_domain = domain_id is not None and domain_id < len(args)
826         has_context = context_id is not None and context_id < len(args)
827
828         domain = args[domain_id] if has_domain else []
829         context = args[context_id] if has_context else {}
830         c, d = eval_context_and_domain(req.session, context, domain)
831         if has_domain:
832             args[domain_id] = d
833         if has_context:
834             args[context_id] = c
835
836         return self._call_kw(req, model, method, args, {})
837     
838     def _call_kw(self, req, model, method, args, kwargs):
839         for i in xrange(len(args)):
840             if isinstance(args[i], web.common.nonliterals.BaseContext):
841                 args[i] = req.session.eval_context(args[i])
842             elif isinstance(args[i], web.common.nonliterals.BaseDomain):
843                 args[i] = req.session.eval_domain(args[i])
844         for k in kwargs.keys():
845             if isinstance(kwargs[k], web.common.nonliterals.BaseContext):
846                 kwargs[k] = req.session.eval_context(kwargs[k])
847             elif isinstance(kwargs[k], web.common.nonliterals.BaseDomain):
848                 kwargs[k] = req.session.eval_domain(kwargs[k])
849
850         return getattr(req.session.model(model), method)(*args, **kwargs)
851
852     @openerpweb.jsonrequest
853     def call(self, req, model, method, args, domain_id=None, context_id=None):
854         return self.call_common(req, model, method, args, domain_id, context_id)
855     
856     @openerpweb.jsonrequest
857     def call_kw(self, req, model, method, args, kwargs):
858         return self._call_kw(req, model, method, args, kwargs)
859
860     @openerpweb.jsonrequest
861     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
862         action = self.call_common(req, model, method, args, domain_id, context_id)
863         if isinstance(action, dict) and action.get('type') != '':
864             return {'result': clean_action(req, action)}
865         return {'result': False}
866
867     @openerpweb.jsonrequest
868     def exec_workflow(self, req, model, id, signal):
869         r = req.session.exec_workflow(model, id, signal)
870         return {'result': r}
871
872     @openerpweb.jsonrequest
873     def default_get(self, req, model, fields):
874         Model = req.session.model(model)
875         return Model.default_get(fields, req.session.eval_context(req.context))
876
877     @openerpweb.jsonrequest
878     def name_search(self, req, model, search_str, domain=[], context={}):
879         m = req.session.model(model)
880         r = m.name_search(search_str+'%', domain, '=ilike', context)
881         return {'result': r}
882
883 class DataGroup(openerpweb.Controller):
884     _cp_path = "/web/group"
885     @openerpweb.jsonrequest
886     def read(self, req, model, fields, group_by_fields, domain=None, sort=None):
887         Model = req.session.model(model)
888         context, domain = eval_context_and_domain(req.session, req.context, domain)
889
890         return Model.read_group(
891             domain or [], fields, group_by_fields, 0, False,
892             dict(context, group_by=group_by_fields), sort or False)
893
894 class View(openerpweb.Controller):
895     _cp_path = "/web/view"
896
897     def fields_view_get(self, req, model, view_id, view_type,
898                         transform=True, toolbar=False, submenu=False):
899         Model = req.session.model(model)
900         context = req.session.eval_context(req.context)
901         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
902         # todo fme?: check that we should pass the evaluated context here
903         self.process_view(req.session, fvg, context, transform, (view_type == 'kanban'))
904         if toolbar and transform:
905             self.process_toolbar(req, fvg['toolbar'])
906         return fvg
907
908     def process_view(self, session, fvg, context, transform, preserve_whitespaces=False):
909         # depending on how it feels, xmlrpclib.ServerProxy can translate
910         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
911         # enjoy unicode strings which can not be trivially converted to
912         # strings, and it blows up during parsing.
913
914         # So ensure we fix this retardation by converting view xml back to
915         # bit strings.
916         if isinstance(fvg['arch'], unicode):
917             arch = fvg['arch'].encode('utf-8')
918         else:
919             arch = fvg['arch']
920
921         if transform:
922             evaluation_context = session.evaluation_context(context or {})
923             xml = self.transform_view(arch, session, evaluation_context)
924         else:
925             xml = ElementTree.fromstring(arch)
926         fvg['arch'] = web.common.xml2json.Xml2Json.convert_element(xml, preserve_whitespaces)
927
928         for field in fvg['fields'].itervalues():
929             if field.get('views'):
930                 for view in field["views"].itervalues():
931                     self.process_view(session, view, None, transform)
932             if field.get('domain'):
933                 field["domain"] = parse_domain(field["domain"], session)
934             if field.get('context'):
935                 field["context"] = parse_context(field["context"], session)
936
937     def process_toolbar(self, req, toolbar):
938         """
939         The toolbar is a mapping of section_key: [action_descriptor]
940
941         We need to clean all those actions in order to ensure correct
942         round-tripping
943         """
944         for actions in toolbar.itervalues():
945             for action in actions:
946                 if 'context' in action:
947                     action['context'] = parse_context(
948                         action['context'], req.session)
949                 if 'domain' in action:
950                     action['domain'] = parse_domain(
951                         action['domain'], req.session)
952
953     @openerpweb.jsonrequest
954     def add_custom(self, req, view_id, arch):
955         CustomView = req.session.model('ir.ui.view.custom')
956         CustomView.create({
957             'user_id': req.session._uid,
958             'ref_id': view_id,
959             'arch': arch
960         }, req.session.eval_context(req.context))
961         return {'result': True}
962
963     @openerpweb.jsonrequest
964     def undo_custom(self, req, view_id, reset=False):
965         CustomView = req.session.model('ir.ui.view.custom')
966         context = req.session.eval_context(req.context)
967         vcustom = CustomView.search([('user_id', '=', req.session._uid), ('ref_id' ,'=', view_id)],
968                                     0, False, False, context)
969         if vcustom:
970             if reset:
971                 CustomView.unlink(vcustom, context)
972             else:
973                 CustomView.unlink([vcustom[0]], context)
974             return {'result': True}
975         return {'result': False}
976
977     def transform_view(self, view_string, session, context=None):
978         # transform nodes on the fly via iterparse, instead of
979         # doing it statically on the parsing result
980         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
981         root = None
982         for event, elem in parser:
983             if event == "start":
984                 if root is None:
985                     root = elem
986                 self.parse_domains_and_contexts(elem, session)
987         return root
988
989     def parse_domains_and_contexts(self, elem, session):
990         """ Converts domains and contexts from the view into Python objects,
991         either literals if they can be parsed by literal_eval or a special
992         placeholder object if the domain or context refers to free variables.
993
994         :param elem: the current node being parsed
995         :type param: xml.etree.ElementTree.Element
996         :param session: OpenERP session object, used to store and retrieve
997                         non-literal objects
998         :type session: openerpweb.openerpweb.OpenERPSession
999         """
1000         for el in ['domain', 'filter_domain']:
1001             domain = elem.get(el, '').strip()
1002             if domain:
1003                 elem.set(el, parse_domain(domain, session))
1004                 elem.set(el + '_string', domain)
1005         for el in ['context', 'default_get']:
1006             context_string = elem.get(el, '').strip()
1007             if context_string:
1008                 elem.set(el, parse_context(context_string, session))
1009                 elem.set(el + '_string', context_string)
1010
1011     @openerpweb.jsonrequest
1012     def load(self, req, model, view_id, view_type, toolbar=False):
1013         return self.fields_view_get(req, model, view_id, view_type, toolbar=toolbar)
1014
1015 def parse_domain(domain, session):
1016     """ Parses an arbitrary string containing a domain, transforms it
1017     to either a literal domain or a :class:`web.common.nonliterals.Domain`
1018
1019     :param domain: the domain to parse, if the domain is not a string it
1020                    is assumed to be a literal domain and is returned as-is
1021     :param session: Current OpenERP session
1022     :type session: openerpweb.openerpweb.OpenERPSession
1023     """
1024     if not isinstance(domain, (str, unicode)):
1025         return domain
1026     try:
1027         return ast.literal_eval(domain)
1028     except ValueError:
1029         # not a literal
1030         return web.common.nonliterals.Domain(session, domain)
1031
1032 def parse_context(context, session):
1033     """ Parses an arbitrary string containing a context, transforms it
1034     to either a literal context or a :class:`web.common.nonliterals.Context`
1035
1036     :param context: the context to parse, if the context is not a string it
1037            is assumed to be a literal domain and is returned as-is
1038     :param session: Current OpenERP session
1039     :type session: openerpweb.openerpweb.OpenERPSession
1040     """
1041     if not isinstance(context, (str, unicode)):
1042         return context
1043     try:
1044         return ast.literal_eval(context)
1045     except ValueError:
1046         return web.common.nonliterals.Context(session, context)
1047
1048 class ListView(View):
1049     _cp_path = "/web/listview"
1050
1051     def process_colors(self, view, row, context):
1052         colors = view['arch']['attrs'].get('colors')
1053
1054         if not colors:
1055             return None
1056
1057         color = [
1058             pair.split(':')[0]
1059             for pair in colors.split(';')
1060             if eval(pair.split(':')[1], dict(context, **row))
1061         ]
1062
1063         if not color:
1064             return None
1065         elif len(color) == 1:
1066             return color[0]
1067         return 'maroon'
1068
1069 class TreeView(View):
1070     _cp_path = "/web/treeview"
1071
1072     @openerpweb.jsonrequest
1073     def action(self, req, model, id):
1074         return load_actions_from_ir_values(
1075             req,'action', 'tree_but_open',[(model, id)],
1076             False)
1077
1078 class SearchView(View):
1079     _cp_path = "/web/searchview"
1080
1081     @openerpweb.jsonrequest
1082     def load(self, req, model, view_id):
1083         fields_view = self.fields_view_get(req, model, view_id, 'search')
1084         return {'fields_view': fields_view}
1085
1086     @openerpweb.jsonrequest
1087     def fields_get(self, req, model):
1088         Model = req.session.model(model)
1089         fields = Model.fields_get(False, req.session.eval_context(req.context))
1090         for field in fields.values():
1091             # shouldn't convert the views too?
1092             if field.get('domain'):
1093                 field["domain"] = parse_domain(field["domain"], req.session)
1094             if field.get('context'):
1095                 field["context"] = parse_context(field["context"], req.session)
1096         return {'fields': fields}
1097
1098     @openerpweb.jsonrequest
1099     def get_filters(self, req, model):
1100         Model = req.session.model("ir.filters")
1101         filters = Model.get_filters(model)
1102         for filter in filters:
1103             filter["context"] = req.session.eval_context(parse_context(filter["context"], req.session))
1104             filter["domain"] = req.session.eval_domain(parse_domain(filter["domain"], req.session))
1105         return filters
1106
1107     @openerpweb.jsonrequest
1108     def save_filter(self, req, model, name, context_to_save, domain):
1109         Model = req.session.model("ir.filters")
1110         ctx = web.common.nonliterals.CompoundContext(context_to_save)
1111         ctx.session = req.session
1112         ctx = ctx.evaluate()
1113         domain = web.common.nonliterals.CompoundDomain(domain)
1114         domain.session = req.session
1115         domain = domain.evaluate()
1116         uid = req.session._uid
1117         context = req.session.eval_context(req.context)
1118         to_return = Model.create_or_replace({"context": ctx,
1119                                              "domain": domain,
1120                                              "model_id": model,
1121                                              "name": name,
1122                                              "user_id": uid
1123                                              }, context)
1124         return to_return
1125
1126     @openerpweb.jsonrequest
1127     def add_to_dashboard(self, req, menu_id, action_id, context_to_save, domain, view_mode, name=''):
1128         ctx = web.common.nonliterals.CompoundContext(context_to_save)
1129         ctx.session = req.session
1130         ctx = ctx.evaluate()
1131         domain = web.common.nonliterals.CompoundDomain(domain)
1132         domain.session = req.session
1133         domain = domain.evaluate()
1134
1135         dashboard_action = load_actions_from_ir_values(req, 'action', 'tree_but_open',
1136                                              [('ir.ui.menu', menu_id)], False)
1137         if dashboard_action:
1138             action = dashboard_action[0][2]
1139             if action['res_model'] == 'board.board' and action['views'][0][1] == 'form':
1140                 # Maybe should check the content instead of model board.board ?
1141                 view_id = action['views'][0][0]
1142                 board = req.session.model(action['res_model']).fields_view_get(view_id, 'form')
1143                 if board and 'arch' in board:
1144                     xml = ElementTree.fromstring(board['arch'])
1145                     column = xml.find('./board/column')
1146                     if column:
1147                         new_action = ElementTree.Element('action', {
1148                                 'name' : str(action_id),
1149                                 'string' : name,
1150                                 'view_mode' : view_mode,
1151                                 'context' : str(ctx),
1152                                 'domain' : str(domain)
1153                             })
1154                         column.insert(0, new_action)
1155                         arch = ElementTree.tostring(xml, 'utf-8')
1156                         return req.session.model('ir.ui.view.custom').create({
1157                                 'user_id': req.session._uid,
1158                                 'ref_id': view_id,
1159                                 'arch': arch
1160                             }, req.session.eval_context(req.context))
1161
1162         return False
1163
1164 class Binary(openerpweb.Controller):
1165     _cp_path = "/web/binary"
1166
1167     @openerpweb.httprequest
1168     def image(self, req, model, id, field, **kw):
1169         Model = req.session.model(model)
1170         context = req.session.eval_context(req.context)
1171
1172         try:
1173             if not id:
1174                 res = Model.default_get([field], context).get(field)
1175             else:
1176                 res = Model.read([int(id)], [field], context)[0].get(field)
1177             image_data = base64.b64decode(res)
1178         except (TypeError, xmlrpclib.Fault):
1179             image_data = self.placeholder(req)
1180         return req.make_response(image_data, [
1181             ('Content-Type', 'image/png'), ('Content-Length', len(image_data))])
1182     def placeholder(self, req):
1183         addons_path = openerpweb.addons_manifest['web']['addons_path']
1184         return open(os.path.join(addons_path, 'web', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
1185
1186     @openerpweb.httprequest
1187     def saveas(self, req, model, field, id=None, filename_field=None, **kw):
1188         """ Download link for files stored as binary fields.
1189
1190         If the ``id`` parameter is omitted, fetches the default value for the
1191         binary field (via ``default_get``), otherwise fetches the field for
1192         that precise record.
1193
1194         :param req: OpenERP request
1195         :type req: :class:`web.common.http.HttpRequest`
1196         :param str model: name of the model to fetch the binary from
1197         :param str field: binary field
1198         :param str id: id of the record from which to fetch the binary
1199         :param str filename_field: field holding the file's name, if any
1200         :returns: :class:`werkzeug.wrappers.Response`
1201         """
1202         Model = req.session.model(model)
1203         context = req.session.eval_context(req.context)
1204         fields = [field]
1205         if filename_field:
1206             fields.append(filename_field)
1207         if id:
1208             res = Model.read([int(id)], fields, context)[0]
1209         else:
1210             res = Model.default_get(fields, context)
1211         filecontent = base64.b64decode(res.get(field, ''))
1212         if not filecontent:
1213             return req.not_found()
1214         else:
1215             filename = '%s_%s' % (model.replace('.', '_'), id)
1216             if filename_field:
1217                 filename = res.get(filename_field, '') or filename
1218             return req.make_response(filecontent,
1219                 [('Content-Type', 'application/octet-stream'),
1220                  ('Content-Disposition', 'attachment; filename=' +  filename)])
1221
1222     @openerpweb.httprequest
1223     def upload(self, req, callback, ufile):
1224         # TODO: might be useful to have a configuration flag for max-length file uploads
1225         try:
1226             out = """<script language="javascript" type="text/javascript">
1227                         var win = window.top.window,
1228                             callback = win[%s];
1229                         if (typeof(callback) === 'function') {
1230                             callback.apply(this, %s);
1231                         } else {
1232                             win.jQuery('#oe_notification', win.document).notify('create', {
1233                                 title: "Ajax File Upload",
1234                                 text: "Could not find callback"
1235                             });
1236                         }
1237                     </script>"""
1238             data = ufile.read()
1239             args = [len(data), ufile.filename,
1240                     ufile.content_type, base64.b64encode(data)]
1241         except Exception, e:
1242             args = [False, e.message]
1243         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1244
1245     @openerpweb.httprequest
1246     def upload_attachment(self, req, callback, model, id, ufile):
1247         context = req.session.eval_context(req.context)
1248         Model = req.session.model('ir.attachment')
1249         try:
1250             out = """<script language="javascript" type="text/javascript">
1251                         var win = window.top.window,
1252                             callback = win[%s];
1253                         if (typeof(callback) === 'function') {
1254                             callback.call(this, %s);
1255                         }
1256                     </script>"""
1257             attachment_id = Model.create({
1258                 'name': ufile.filename,
1259                 'datas': base64.encodestring(ufile.read()),
1260                 'res_model': model,
1261                 'res_id': int(id)
1262             }, context)
1263             args = {
1264                 'filename': ufile.filename,
1265                 'id':  attachment_id
1266             }
1267         except Exception, e:
1268             args = { 'error': e.message }
1269         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1270
1271 class Action(openerpweb.Controller):
1272     _cp_path = "/web/action"
1273
1274     @openerpweb.jsonrequest
1275     def load(self, req, action_id, do_not_eval=False):
1276         Actions = req.session.model('ir.actions.actions')
1277         value = False
1278         context = req.session.eval_context(req.context)
1279         action_type = Actions.read([action_id], ['type'], context)
1280         if action_type:
1281             ctx = {}
1282             if action_type[0]['type'] == 'ir.actions.report.xml':
1283                 ctx.update({'bin_size': True})
1284             ctx.update(context)
1285             action = req.session.model(action_type[0]['type']).read([action_id], False, ctx)
1286             if action:
1287                 value = clean_action(req, action[0], do_not_eval)
1288         return {'result': value}
1289
1290     @openerpweb.jsonrequest
1291     def run(self, req, action_id):
1292         return clean_action(req, req.session.model('ir.actions.server').run(
1293             [action_id], req.session.eval_context(req.context)))
1294
1295 class Export(View):
1296     _cp_path = "/web/export"
1297
1298     @openerpweb.jsonrequest
1299     def formats(self, req):
1300         """ Returns all valid export formats
1301
1302         :returns: for each export format, a pair of identifier and printable name
1303         :rtype: [(str, str)]
1304         """
1305         return sorted([
1306             controller.fmt
1307             for path, controller in openerpweb.controllers_path.iteritems()
1308             if path.startswith(self._cp_path)
1309             if hasattr(controller, 'fmt')
1310         ], key=operator.itemgetter(1))
1311
1312     def fields_get(self, req, model):
1313         Model = req.session.model(model)
1314         fields = Model.fields_get(False, req.session.eval_context(req.context))
1315         return fields
1316
1317     @openerpweb.jsonrequest
1318     def get_fields(self, req, model, prefix='', parent_name= '',
1319                    import_compat=True, parent_field_type=None,
1320                    exclude=None):
1321
1322         if import_compat and parent_field_type == "many2one":
1323             fields = {}
1324         else:
1325             fields = self.fields_get(req, model)
1326
1327         if import_compat:
1328             fields.pop('id', None)
1329         else:
1330             fields['.id'] = fields.pop('id', {'string': 'ID'})
1331
1332         fields_sequence = sorted(fields.iteritems(),
1333             key=lambda field: field[1].get('string', ''))
1334
1335         records = []
1336         for field_name, field in fields_sequence:
1337             if import_compat:
1338                 if exclude and field_name in exclude:
1339                     continue
1340                 if 'function' in field:
1341                     continue
1342                 if field.get('readonly'):
1343                     # If none of the field's states unsets readonly, skip the field
1344                     if all(dict(attrs).get('readonly', True)
1345                            for attrs in field.get('states', {}).values()):
1346                         continue
1347
1348             id = prefix + (prefix and '/'or '') + field_name
1349             name = parent_name + (parent_name and '/' or '') + field['string']
1350             record = {'id': id, 'string': name,
1351                       'value': id, 'children': False,
1352                       'field_type': field.get('type'),
1353                       'required': field.get('required'),
1354                       'relation_field': field.get('relation_field')}
1355             records.append(record)
1356
1357             if len(name.split('/')) < 3 and 'relation' in field:
1358                 ref = field.pop('relation')
1359                 record['value'] += '/id'
1360                 record['params'] = {'model': ref, 'prefix': id, 'name': name}
1361
1362                 if not import_compat or field['type'] == 'one2many':
1363                     # m2m field in import_compat is childless
1364                     record['children'] = True
1365
1366         return records
1367
1368     @openerpweb.jsonrequest
1369     def namelist(self,req,  model, export_id):
1370         # TODO: namelist really has no reason to be in Python (although itertools.groupby helps)
1371         export = req.session.model("ir.exports").read([export_id])[0]
1372         export_fields_list = req.session.model("ir.exports.line").read(
1373             export['export_fields'])
1374
1375         fields_data = self.fields_info(
1376             req, model, map(operator.itemgetter('name'), export_fields_list))
1377
1378         return [
1379             {'name': field['name'], 'label': fields_data[field['name']]}
1380             for field in export_fields_list
1381         ]
1382
1383     def fields_info(self, req, model, export_fields):
1384         info = {}
1385         fields = self.fields_get(req, model)
1386
1387         # To make fields retrieval more efficient, fetch all sub-fields of a
1388         # given field at the same time. Because the order in the export list is
1389         # arbitrary, this requires ordering all sub-fields of a given field
1390         # together so they can be fetched at the same time
1391         #
1392         # Works the following way:
1393         # * sort the list of fields to export, the default sorting order will
1394         #   put the field itself (if present, for xmlid) and all of its
1395         #   sub-fields right after it
1396         # * then, group on: the first field of the path (which is the same for
1397         #   a field and for its subfields and the length of splitting on the
1398         #   first '/', which basically means grouping the field on one side and
1399         #   all of the subfields on the other. This way, we have the field (for
1400         #   the xmlid) with length 1, and all of the subfields with the same
1401         #   base but a length "flag" of 2
1402         # * if we have a normal field (length 1), just add it to the info
1403         #   mapping (with its string) as-is
1404         # * otherwise, recursively call fields_info via graft_subfields.
1405         #   all graft_subfields does is take the result of fields_info (on the
1406         #   field's model) and prepend the current base (current field), which
1407         #   rebuilds the whole sub-tree for the field
1408         #
1409         # result: because we're not fetching the fields_get for half the
1410         # database models, fetching a namelist with a dozen fields (including
1411         # relational data) falls from ~6s to ~300ms (on the leads model).
1412         # export lists with no sub-fields (e.g. import_compatible lists with
1413         # no o2m) are even more efficient (from the same 6s to ~170ms, as
1414         # there's a single fields_get to execute)
1415         for (base, length), subfields in itertools.groupby(
1416                 sorted(export_fields),
1417                 lambda field: (field.split('/', 1)[0], len(field.split('/', 1)))):
1418             subfields = list(subfields)
1419             if length == 2:
1420                 # subfields is a seq of $base/*rest, and not loaded yet
1421                 info.update(self.graft_subfields(
1422                     req, fields[base]['relation'], base, fields[base]['string'],
1423                     subfields
1424                 ))
1425             else:
1426                 info[base] = fields[base]['string']
1427
1428         return info
1429
1430     def graft_subfields(self, req, model, prefix, prefix_string, fields):
1431         export_fields = [field.split('/', 1)[1] for field in fields]
1432         return (
1433             (prefix + '/' + k, prefix_string + '/' + v)
1434             for k, v in self.fields_info(req, model, export_fields).iteritems())
1435
1436     #noinspection PyPropertyDefinition
1437     @property
1438     def content_type(self):
1439         """ Provides the format's content type """
1440         raise NotImplementedError()
1441
1442     def filename(self, base):
1443         """ Creates a valid filename for the format (with extension) from the
1444          provided base name (exension-less)
1445         """
1446         raise NotImplementedError()
1447
1448     def from_data(self, fields, rows):
1449         """ Conversion method from OpenERP's export data to whatever the
1450         current export class outputs
1451
1452         :params list fields: a list of fields to export
1453         :params list rows: a list of records to export
1454         :returns:
1455         :rtype: bytes
1456         """
1457         raise NotImplementedError()
1458
1459     @openerpweb.httprequest
1460     def index(self, req, data, token):
1461         model, fields, ids, domain, import_compat = \
1462             operator.itemgetter('model', 'fields', 'ids', 'domain',
1463                                 'import_compat')(
1464                 simplejson.loads(data))
1465
1466         context = req.session.eval_context(req.context)
1467         Model = req.session.model(model)
1468         ids = ids or Model.search(domain, 0, False, False, context)
1469
1470         field_names = map(operator.itemgetter('name'), fields)
1471         import_data = Model.export_data(ids, field_names, context).get('datas',[])
1472
1473         if import_compat:
1474             columns_headers = field_names
1475         else:
1476             columns_headers = [val['label'].strip() for val in fields]
1477
1478
1479         return req.make_response(self.from_data(columns_headers, import_data),
1480             headers=[('Content-Disposition', 'attachment; filename="%s"' % self.filename(model)),
1481                      ('Content-Type', self.content_type)],
1482             cookies={'fileToken': int(token)})
1483
1484 class CSVExport(Export):
1485     _cp_path = '/web/export/csv'
1486     fmt = ('csv', 'CSV')
1487
1488     @property
1489     def content_type(self):
1490         return 'text/csv;charset=utf8'
1491
1492     def filename(self, base):
1493         return base + '.csv'
1494
1495     def from_data(self, fields, rows):
1496         fp = StringIO()
1497         writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1498
1499         writer.writerow(fields)
1500
1501         for data in rows:
1502             row = []
1503             for d in data:
1504                 if isinstance(d, basestring):
1505                     d = d.replace('\n',' ').replace('\t',' ')
1506                     try:
1507                         d = d.encode('utf-8')
1508                     except:
1509                         pass
1510                 if d is False: d = None
1511                 row.append(d)
1512             writer.writerow(row)
1513
1514         fp.seek(0)
1515         data = fp.read()
1516         fp.close()
1517         return data
1518
1519 class ExcelExport(Export):
1520     _cp_path = '/web/export/xls'
1521     fmt = ('xls', 'Excel')
1522
1523     @property
1524     def content_type(self):
1525         return 'application/vnd.ms-excel'
1526
1527     def filename(self, base):
1528         return base + '.xls'
1529
1530     def from_data(self, fields, rows):
1531         import xlwt
1532
1533         workbook = xlwt.Workbook()
1534         worksheet = workbook.add_sheet('Sheet 1')
1535
1536         for i, fieldname in enumerate(fields):
1537             worksheet.write(0, i, str(fieldname))
1538             worksheet.col(i).width = 8000 # around 220 pixels
1539
1540         style = xlwt.easyxf('align: wrap yes')
1541
1542         for row_index, row in enumerate(rows):
1543             for cell_index, cell_value in enumerate(row):
1544                 if isinstance(cell_value, basestring):
1545                     cell_value = re.sub("\r", " ", cell_value)
1546                 if cell_value is False: cell_value = None
1547                 worksheet.write(row_index + 1, cell_index, cell_value, style)
1548
1549         fp = StringIO()
1550         workbook.save(fp)
1551         fp.seek(0)
1552         data = fp.read()
1553         fp.close()
1554         return data
1555
1556 class Reports(View):
1557     _cp_path = "/web/report"
1558     POLLING_DELAY = 0.25
1559     TYPES_MAPPING = {
1560         'doc': 'application/vnd.ms-word',
1561         'html': 'text/html',
1562         'odt': 'application/vnd.oasis.opendocument.text',
1563         'pdf': 'application/pdf',
1564         'sxw': 'application/vnd.sun.xml.writer',
1565         'xls': 'application/vnd.ms-excel',
1566     }
1567
1568     @openerpweb.httprequest
1569     def index(self, req, action, token):
1570         action = simplejson.loads(action)
1571
1572         report_srv = req.session.proxy("report")
1573         context = req.session.eval_context(
1574             web.common.nonliterals.CompoundContext(
1575                 req.context or {}, action[ "context"]))
1576
1577         report_data = {}
1578         report_ids = context["active_ids"]
1579         if 'report_type' in action:
1580             report_data['report_type'] = action['report_type']
1581         if 'datas' in action:
1582             if 'ids' in action['datas']:
1583                 report_ids = action['datas'].pop('ids')
1584             report_data.update(action['datas'])
1585
1586         report_id = report_srv.report(
1587             req.session._db, req.session._uid, req.session._password,
1588             action["report_name"], report_ids,
1589             report_data, context)
1590
1591         report_struct = None
1592         while True:
1593             report_struct = report_srv.report_get(
1594                 req.session._db, req.session._uid, req.session._password, report_id)
1595             if report_struct["state"]:
1596                 break
1597
1598             time.sleep(self.POLLING_DELAY)
1599
1600         report = base64.b64decode(report_struct['result'])
1601         if report_struct.get('code') == 'zlib':
1602             report = zlib.decompress(report)
1603         report_mimetype = self.TYPES_MAPPING.get(
1604             report_struct['format'], 'octet-stream')
1605         return req.make_response(report,
1606              headers=[
1607                  ('Content-Disposition', 'attachment; filename="%s.%s"' % (action['report_name'], report_struct['format'])),
1608                  ('Content-Type', report_mimetype),
1609                  ('Content-Length', len(report))],
1610              cookies={'fileToken': int(token)})
1611
1612 class Import(View):
1613     _cp_path = "/web/import"
1614
1615     def fields_get(self, req, model):
1616         Model = req.session.model(model)
1617         fields = Model.fields_get(False, req.session.eval_context(req.context))
1618         return fields
1619
1620     @openerpweb.httprequest
1621     def detect_data(self, req, csvfile, csvsep=',', csvdel='"', csvcode='utf-8', jsonp='callback'):
1622         try:
1623             data = list(csv.reader(
1624                 csvfile, quotechar=str(csvdel), delimiter=str(csvsep)))
1625         except csv.Error, e:
1626             csvfile.seek(0)
1627             return '<script>window.top.%s(%s);</script>' % (
1628                 jsonp, simplejson.dumps({'error': {
1629                     'message': 'Error parsing CSV file: %s' % e,
1630                     # decodes each byte to a unicode character, which may or
1631                     # may not be printable, but decoding will succeed.
1632                     # Otherwise simplejson will try to decode the `str` using
1633                     # utf-8, which is very likely to blow up on characters out
1634                     # of the ascii range (in range [128, 256))
1635                     'preview': csvfile.read(200).decode('iso-8859-1')}}))
1636
1637         try:
1638             return '<script>window.top.%s(%s);</script>' % (
1639                 jsonp, simplejson.dumps(
1640                     {'records': data[:10]}, encoding=csvcode))
1641         except UnicodeDecodeError:
1642             return '<script>window.top.%s(%s);</script>' % (
1643                 jsonp, simplejson.dumps({
1644                     'message': u"Failed to decode CSV file using encoding %s, "
1645                                u"try switching to a different encoding" % csvcode
1646                 }))
1647
1648     @openerpweb.httprequest
1649     def import_data(self, req, model, csvfile, csvsep, csvdel, csvcode, jsonp,
1650                     meta):
1651         modle_obj = req.session.model(model)
1652         skip, indices, fields = operator.itemgetter('skip', 'indices', 'fields')(
1653             simplejson.loads(meta))
1654
1655         error = None
1656         if not (csvdel and len(csvdel) == 1):
1657             error = u"The CSV delimiter must be a single character"
1658
1659         if not indices and fields:
1660             error = u"You must select at least one field to import"
1661
1662         if error:
1663             return '<script>window.top.%s(%s);</script>' % (
1664                 jsonp, simplejson.dumps({'error': {'message': error}}))
1665
1666         # skip ignored records
1667         data_record = itertools.islice(
1668             csv.reader(csvfile, quotechar=str(csvdel), delimiter=str(csvsep)),
1669             skip, None)
1670
1671         # if only one index, itemgetter will return an atom rather than a tuple
1672         if len(indices) == 1: mapper = lambda row: [row[indices[0]]]
1673         else: mapper = operator.itemgetter(*indices)
1674
1675         data = None
1676         error = None
1677         try:
1678             # decode each data row
1679             data = [
1680                 [record.decode(csvcode) for record in row]
1681                 for row in itertools.imap(mapper, data_record)
1682                 # don't insert completely empty rows (can happen due to fields
1683                 # filtering in case of e.g. o2m content rows)
1684                 if any(row)
1685             ]
1686         except UnicodeDecodeError:
1687             error = u"Failed to decode CSV file using encoding %s" % csvcode
1688         except csv.Error, e:
1689             error = u"Could not process CSV file: %s" % e
1690
1691         # If the file contains nothing,
1692         if not data:
1693             error = u"File to import is empty"
1694         if error:
1695             return '<script>window.top.%s(%s);</script>' % (
1696                 jsonp, simplejson.dumps({'error': {'message': error}}))
1697
1698         try:
1699             (code, record, message, _nope) = modle_obj.import_data(
1700                 fields, data, 'init', '', False,
1701                 req.session.eval_context(req.context))
1702         except xmlrpclib.Fault, e:
1703             error = {"message": u"%s, %s" % (e.faultCode, e.faultString)}
1704             return '<script>window.top.%s(%s);</script>' % (
1705                 jsonp, simplejson.dumps({'error':error}))
1706
1707         if code != -1:
1708             return '<script>window.top.%s(%s);</script>' % (
1709                 jsonp, simplejson.dumps({'success':True}))
1710
1711         msg = u"Error during import: %s\n\nTrying to import record %r" % (
1712             message, record)
1713         return '<script>window.top.%s(%s);</script>' % (
1714             jsonp, simplejson.dumps({'error': {'message':msg}}))