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