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