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