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