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