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