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