[merge]
[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, context=context))
410             for id, name, action in actions]
411
412 def clean_action(action, session, context=None):
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(context=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
579         context, domain = eval_context_and_domain(
580             request.session, request.context, domain)
581
582         ids = Model.search(domain, 0, False, sort or False, context)
583         # need to fill the dataset with all ids for the (domain, context) pair,
584         # so search un-paginated and paginate manually before reading
585         paginated_ids = ids[offset:(offset + limit if limit else None)]
586         if fields and fields == ['id']:
587             # shortcut read if we only want the ids
588             return {
589                 'ids': ids,
590                 'records': map(lambda id: {'id': id}, paginated_ids)
591             }
592
593         records = Model.read(paginated_ids, fields or False, context)
594         records.sort(key=lambda obj: ids.index(obj['id']))
595         return {
596             'ids': ids,
597             'records': records
598         }
599
600
601     @openerpweb.jsonrequest
602     def read(self, request, model, ids, fields=False):
603         return self.do_search_read(request, model, ids, fields)
604
605     @openerpweb.jsonrequest
606     def get(self, request, model, ids, fields=False):
607         return self.do_get(request, model, ids, fields)
608
609     def do_get(self, request, model, ids, fields=False):
610         """ Fetches and returns the records of the model ``model`` whose ids
611         are in ``ids``.
612
613         The results are in the same order as the inputs, but elements may be
614         missing (if there is no record left for the id)
615
616         :param request: the JSON-RPC2 request object
617         :type request: openerpweb.JsonRequest
618         :param model: the model to read from
619         :type model: str
620         :param ids: a list of identifiers
621         :type ids: list
622         :param fields: a list of fields to fetch, ``False`` or empty to fetch
623                        all fields in the model
624         :type fields: list | False
625         :returns: a list of records, in the same order as the list of ids
626         :rtype: list
627         """
628         Model = request.session.model(model)
629         records = Model.read(ids, fields, request.session.eval_context(request.context))
630
631         record_map = dict((record['id'], record) for record in records)
632
633         return [record_map[id] for id in ids if record_map.get(id)]
634
635     @openerpweb.jsonrequest
636     def load(self, req, model, id, fields):
637         m = req.session.model(model)
638         value = {}
639         r = m.read([id], False, req.session.eval_context(req.context))
640         if r:
641             value = r[0]
642         return {'value': value}
643
644     @openerpweb.jsonrequest
645     def create(self, req, model, data):
646         m = req.session.model(model)
647         r = m.create(data, req.session.eval_context(req.context))
648         return {'result': r}
649
650     @openerpweb.jsonrequest
651     def save(self, req, model, id, data):
652         m = req.session.model(model)
653         r = m.write([id], data, req.session.eval_context(req.context))
654         return {'result': r}
655
656     @openerpweb.jsonrequest
657     def unlink(self, request, model, ids=()):
658         Model = request.session.model(model)
659         return Model.unlink(ids, request.session.eval_context(request.context))
660
661     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
662         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
663         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
664         c, d = eval_context_and_domain(req.session, context, domain)
665         if domain_id and len(args) - 1 >= domain_id:
666             args[domain_id] = d
667         if context_id and len(args) - 1 >= context_id:
668             args[context_id] = c
669
670         return getattr(req.session.model(model), method)(*args)
671
672     @openerpweb.jsonrequest
673     def call(self, req, model, method, args, domain_id=None, context_id=None):
674         return self.call_common(req, model, method, args, domain_id, context_id)
675
676     @openerpweb.jsonrequest
677     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
678         action = self.call_common(req, model, method, args, domain_id, context_id)
679         if isinstance(action, dict) and action.get('type') != '':
680             return {'result': clean_action(action, req.session)}
681         return {'result': False}
682
683     @openerpweb.jsonrequest
684     def exec_workflow(self, req, model, id, signal):
685         r = req.session.exec_workflow(model, id, signal)
686         return {'result': r}
687
688     @openerpweb.jsonrequest
689     def default_get(self, req, model, fields):
690         Model = req.session.model(model)
691         return Model.default_get(fields, req.session.eval_context(req.context))
692
693     @openerpweb.jsonrequest
694     def name_search(self, req, model, search_str, domain=[], context={}):
695         m = req.session.model(model)
696         r = m.name_search(search_str+'%', domain, '=ilike', context)
697         return {'result': r}
698
699 class DataGroup(openerpweb.Controller):
700     _cp_path = "/base/group"
701     @openerpweb.jsonrequest
702     def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
703         Model = request.session.model(model)
704         context, domain = eval_context_and_domain(request.session, request.context, domain)
705
706         return Model.read_group(
707             domain or [], fields, group_by_fields, 0, False,
708             dict(context, group_by=group_by_fields), sort or False)
709
710 class View(openerpweb.Controller):
711     _cp_path = "/base/view"
712
713     def fields_view_get(self, request, model, view_id, view_type,
714                         transform=True, toolbar=False, submenu=False):
715         Model = request.session.model(model)
716         context = request.session.eval_context(request.context)
717         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
718         # todo fme?: check that we should pass the evaluated context here
719         self.process_view(request.session, fvg, context, transform)
720         return fvg
721
722     def process_view(self, session, fvg, context, transform):
723         # depending on how it feels, xmlrpclib.ServerProxy can translate
724         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
725         # enjoy unicode strings which can not be trivially converted to
726         # strings, and it blows up during parsing.
727
728         # So ensure we fix this retardation by converting view xml back to
729         # bit strings.
730         if isinstance(fvg['arch'], unicode):
731             arch = fvg['arch'].encode('utf-8')
732         else:
733             arch = fvg['arch']
734
735         if transform:
736             evaluation_context = session.evaluation_context(context or {})
737             xml = self.transform_view(arch, session, evaluation_context)
738         else:
739             xml = ElementTree.fromstring(arch)
740         fvg['arch'] = Xml2Json.convert_element(xml)
741
742         for field in fvg['fields'].itervalues():
743             if field.get('views'):
744                 for view in field["views"].itervalues():
745                     self.process_view(session, view, None, transform)
746             if field.get('domain'):
747                 field["domain"] = self.parse_domain(field["domain"], session)
748             if field.get('context'):
749                 field["context"] = self.parse_context(field["context"], session)
750
751     @openerpweb.jsonrequest
752     def add_custom(self, request, view_id, arch):
753         CustomView = request.session.model('ir.ui.view.custom')
754         CustomView.create({
755             'user_id': request.session._uid,
756             'ref_id': view_id,
757             'arch': arch
758         }, request.session.eval_context(request.context))
759         return {'result': True}
760
761     @openerpweb.jsonrequest
762     def undo_custom(self, request, view_id, reset=False):
763         CustomView = request.session.model('ir.ui.view.custom')
764         context = request.session.eval_context(request.context)
765         vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
766                                     0, False, False, context)
767         if vcustom:
768             if reset:
769                 CustomView.unlink(vcustom, context)
770             else:
771                 CustomView.unlink([vcustom[0]], context)
772             return {'result': True}
773         return {'result': False}
774
775     def transform_view(self, view_string, session, context=None):
776         # transform nodes on the fly via iterparse, instead of
777         # doing it statically on the parsing result
778         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
779         root = None
780         for event, elem in parser:
781             if event == "start":
782                 if root is None:
783                     root = elem
784                 self.parse_domains_and_contexts(elem, session)
785         return root
786
787     def parse_domain(self, domain, session):
788         """ Parses an arbitrary string containing a domain, transforms it
789         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
790
791         :param domain: the domain to parse, if the domain is not a string it is assumed to
792         be a literal domain and is returned as-is
793         :param session: Current OpenERP session
794         :type session: openerpweb.openerpweb.OpenERPSession
795         """
796         if not isinstance(domain, (str, unicode)):
797             return domain
798         try:
799             return openerpweb.ast.literal_eval(domain)
800         except ValueError:
801             # not a literal
802             return openerpweb.nonliterals.Domain(session, domain)
803
804     def parse_context(self, context, session):
805         """ Parses an arbitrary string containing a context, transforms it
806         to either a literal context or a :class:`openerpweb.nonliterals.Context`
807
808         :param context: the context to parse, if the context is not a string it is assumed to
809         be a literal domain and is returned as-is
810         :param session: Current OpenERP session
811         :type session: openerpweb.openerpweb.OpenERPSession
812         """
813         if not isinstance(context, (str, unicode)):
814             return context
815         try:
816             return openerpweb.ast.literal_eval(context)
817         except ValueError:
818             return openerpweb.nonliterals.Context(session, context)
819
820     def parse_domains_and_contexts(self, elem, session):
821         """ Converts domains and contexts from the view into Python objects,
822         either literals if they can be parsed by literal_eval or a special
823         placeholder object if the domain or context refers to free variables.
824
825         :param elem: the current node being parsed
826         :type param: xml.etree.ElementTree.Element
827         :param session: OpenERP session object, used to store and retrieve
828                         non-literal objects
829         :type session: openerpweb.openerpweb.OpenERPSession
830         """
831         for el in ['domain', 'filter_domain']:
832             domain = elem.get(el, '').strip()
833             if domain:
834                 elem.set(el, self.parse_domain(domain, session))
835         for el in ['context', 'default_get']:
836             context_string = elem.get(el, '').strip()
837             if context_string:
838                 elem.set(el, self.parse_context(context_string, session))
839
840 class FormView(View):
841     _cp_path = "/base/formview"
842
843     @openerpweb.jsonrequest
844     def load(self, req, model, view_id, toolbar=False):
845         fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
846         return {'fields_view': fields_view}
847
848 class ListView(View):
849     _cp_path = "/base/listview"
850
851     @openerpweb.jsonrequest
852     def load(self, req, model, view_id, toolbar=False):
853         fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
854         return {'fields_view': fields_view}
855
856     def process_colors(self, view, row, context):
857         colors = view['arch']['attrs'].get('colors')
858
859         if not colors:
860             return None
861
862         color = [
863             pair.split(':')[0]
864             for pair in colors.split(';')
865             if eval(pair.split(':')[1], dict(context, **row))
866         ]
867
868         if not color:
869             return None
870         elif len(color) == 1:
871             return color[0]
872         return 'maroon'
873
874 class SearchView(View):
875     _cp_path = "/base/searchview"
876
877     @openerpweb.jsonrequest
878     def load(self, req, model, view_id):
879         fields_view = self.fields_view_get(req, model, view_id, 'search')
880         return {'fields_view': fields_view}
881
882     @openerpweb.jsonrequest
883     def fields_get(self, req, model):
884         Model = req.session.model(model)
885         fields = Model.fields_get(False, req.session.eval_context(req.context))
886         for field in fields.values():
887             # shouldn't convert the views too?
888             if field.get('domain'):
889                 field["domain"] = self.parse_domain(field["domain"], req.session)
890             if field.get('context'):
891                 field["context"] = self.parse_domain(field["context"], req.session)
892         return {'fields': fields}
893     
894     @openerpweb.jsonrequest
895     def get_filters(self, req, model):
896         Model = req.session.model("ir.filters")
897         filters = Model.get_filters(model)
898         for filter in filters:
899             filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
900             filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
901         return filters
902     
903     @openerpweb.jsonrequest
904     def save_filter(self, req, model, name, context_to_save, domain):
905         Model = req.session.model("ir.filters")
906         ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
907         ctx.session = req.session
908         ctx = ctx.evaluate()
909         domain = openerpweb.nonliterals.CompoundDomain(domain)
910         domain.session = req.session
911         domain = domain.evaluate()
912         uid = req.session._uid
913         context = req.session.eval_context(req.context)
914         to_return = Model.create_or_replace({"context": ctx,
915                                              "domain": domain,
916                                              "model_id": model,
917                                              "name": name,
918                                              "user_id": uid
919                                              }, context)
920         return to_return
921
922 class Binary(openerpweb.Controller):
923     _cp_path = "/base/binary"
924
925     @openerpweb.httprequest
926     def image(self, request, session_id, model, id, field, **kw):
927         cherrypy.response.headers['Content-Type'] = 'image/png'
928         Model = request.session.model(model)
929         context = request.session.eval_context(request.context)
930         try:
931             if not id:
932                 res = Model.default_get([field], context).get(field, '')
933             else:
934                 res = Model.read([int(id)], [field], context)[0].get(field, '')
935             return base64.decodestring(res)
936         except: # TODO: what's the exception here?
937             return self.placeholder()
938     def placeholder(self):
939         return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
940
941     @openerpweb.httprequest
942     def saveas(self, request, session_id, model, id, field, fieldname, **kw):
943         Model = request.session.model(model)
944         context = request.session.eval_context(request.context)
945         res = Model.read([int(id)], [field, fieldname], context)[0]
946         filecontent = res.get(field, '')
947         if not filecontent:
948             raise cherrypy.NotFound
949         else:
950             cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
951             filename = '%s_%s' % (model.replace('.', '_'), id)
952             if fieldname:
953                 filename = res.get(fieldname, '') or filename
954             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' +  filename
955             return base64.decodestring(filecontent)
956
957     @openerpweb.httprequest
958     def upload(self, request, session_id, callback, ufile=None):
959         cherrypy.response.timeout = 500
960         headers = {}
961         for key, val in cherrypy.request.headers.iteritems():
962             headers[key.lower()] = val
963         size = int(headers.get('content-length', 0))
964         # TODO: might be useful to have a configuration flag for max-length file uploads
965         try:
966             out = """<script language="javascript" type="text/javascript">
967                         var win = window.top.window,
968                             callback = win[%s];
969                         if (typeof(callback) === 'function') {
970                             callback.apply(this, %s);
971                         } else {
972                             win.jQuery('#oe_notification', win.document).notify('create', {
973                                 title: "Ajax File Upload",
974                                 text: "Could not find callback"
975                             });
976                         }
977                     </script>"""
978             data = ufile.file.read()
979             args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
980         except Exception, e:
981             args = [False, e.message]
982         return out % (simplejson.dumps(callback), simplejson.dumps(args))
983
984     @openerpweb.httprequest
985     def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
986         cherrypy.response.timeout = 500
987         context = request.session.eval_context(request.context)
988         Model = request.session.model('ir.attachment')
989         try:
990             out = """<script language="javascript" type="text/javascript">
991                         var win = window.top.window,
992                             callback = win[%s];
993                         if (typeof(callback) === 'function') {
994                             callback.call(this, %s);
995                         }
996                     </script>"""
997             attachment_id = Model.create({
998                 'name': ufile.filename,
999                 'datas': base64.encodestring(ufile.file.read()),
1000                 'res_model': model,
1001                 'res_id': int(id)
1002             }, context)
1003             args = {
1004                 'filename': ufile.filename,
1005                 'id':  attachment_id
1006             }
1007         except Exception, e:
1008             args = { 'error': e.message }
1009         return out % (simplejson.dumps(callback), simplejson.dumps(args))
1010
1011 class Action(openerpweb.Controller):
1012     _cp_path = "/base/action"
1013
1014     @openerpweb.jsonrequest
1015     def load(self, req, action_id):
1016         Actions = req.session.model('ir.actions.actions')
1017         value = False
1018         context = req.session.eval_context(req.context)
1019         action_type = Actions.read([action_id], ['type'], context)
1020         if action_type:
1021             action = req.session.model(action_type[0]['type']).read([action_id], False,
1022                                                                     context)
1023             if action:
1024                 value = clean_action(action[0], req.session)
1025         return {'result': value}
1026
1027     @openerpweb.jsonrequest
1028     def run(self, req, action_id):
1029         return clean_action(req.session.model('ir.actions.server').run(
1030             [action_id], req.session.eval_context(req.context)), req.session)
1031
1032 class TreeView(View):
1033     _cp_path = "/base/treeview"
1034
1035     @openerpweb.jsonrequest
1036     def load(self, req, model, view_id, toolbar=False):
1037         return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1038
1039     @openerpweb.jsonrequest
1040     def action(self, req, model, id):
1041         return load_actions_from_ir_values(
1042             req,'action', 'tree_but_open',[(model, id)],
1043             False, req.session.eval_context(req.context))
1044
1045 def export_csv(fields, result):
1046     fp = StringIO()
1047     writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1048
1049     writer.writerow(fields)
1050
1051     for data in result:
1052         row = []
1053         for d in data:
1054             if isinstance(d, basestring):
1055                 d = d.replace('\n',' ').replace('\t',' ')
1056                 try:
1057                     d = d.encode('utf-8')
1058                 except:
1059                     pass
1060             if d is False: d = None
1061             row.append(d)
1062         writer.writerow(row)
1063
1064     fp.seek(0)
1065     data = fp.read()
1066     fp.close()
1067     return data
1068
1069 def export_xls(fieldnames, table):
1070     try:
1071         import xlwt
1072     except ImportError:
1073         common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1074
1075     workbook = xlwt.Workbook()
1076     worksheet = workbook.add_sheet('Sheet 1')
1077
1078     for i, fieldname in enumerate(fieldnames):
1079         worksheet.write(0, i, str(fieldname))
1080         worksheet.col(i).width = 8000 # around 220 pixels
1081
1082     style = xlwt.easyxf('align: wrap yes')
1083
1084     for row_index, row in enumerate(table):
1085         for cell_index, cell_value in enumerate(row):
1086             cell_value = str(cell_value)
1087             cell_value = re.sub("\r", " ", cell_value)
1088             worksheet.write(row_index + 1, cell_index, cell_value, style)
1089
1090
1091     fp = StringIO()
1092     workbook.save(fp)
1093     fp.seek(0)
1094     data = fp.read()
1095     fp.close()
1096     #return data.decode('ISO-8859-1')
1097     return unicode(data, 'utf-8', 'replace')
1098
1099 class Export(View):
1100     _cp_path = "/base/export"
1101
1102     def fields_get(self, req, model):
1103         Model = req.session.model(model)
1104         fields = Model.fields_get(False, req.session.eval_context(req.context))
1105         return fields
1106
1107     @openerpweb.jsonrequest
1108     def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1109         import_compat = params.get("import_compat", False)
1110
1111         fields = self.fields_get(req, model)
1112         field_parent_type = params.get("parent_field_type",False)
1113
1114         if import_compat and field_parent_type and field_parent_type == "many2one":
1115             fields = {}
1116
1117         fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1118         records = []
1119         fields_order = fields.keys()
1120         fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1121
1122         for index, field in enumerate(fields_order):
1123             value = fields[field]
1124             record = {}
1125             if import_compat and value.get('readonly', False):
1126                 ok = False
1127                 for sl in value.get('states', {}).values():
1128                     for s in sl:
1129                         ok = ok or (s==['readonly',False])
1130                 if not ok: continue
1131
1132             id = prefix + (prefix and '/'or '') + field
1133             nm = name + (name and '/' or '') + value['string']
1134             record.update(id=id, string= nm, action='javascript: void(0)',
1135                           target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1136             records.append(record)
1137
1138             if len(nm.split('/')) < 3 and value.get('relation', False):
1139                 if import_compat:
1140                     ref = value.pop('relation')
1141                     cfields = self.fields_get(req, ref)
1142                     if (value['type'] == 'many2many'):
1143                         record['children'] = []
1144                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1145
1146                     elif value['type'] == 'many2one':
1147                         record['children'] = [id + '/id', id + '/.id']
1148                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1149
1150                     else:
1151                         cfields_order = cfields.keys()
1152                         cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1153                         children = []
1154                         for j, fld in enumerate(cfields_order):
1155                             cid = id + '/' + fld
1156                             cid = cid.replace(' ', '_')
1157                             children.append(cid)
1158                         record['children'] = children or []
1159                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1160                 else:
1161                     ref = value.pop('relation')
1162                     cfields = self.fields_get(req, ref)
1163                     cfields_order = cfields.keys()
1164                     cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1165                     children = []
1166                     for j, fld in enumerate(cfields_order):
1167                         cid = id + '/' + fld
1168                         cid = cid.replace(' ', '_')
1169                         children.append(cid)
1170                     record['children'] = children or []
1171                     record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1172
1173         records.reverse()
1174         return records
1175
1176     @openerpweb.jsonrequest
1177     def save_export_lists(self, req, name, model, field_list):
1178         result = {'resource':model, 'name':name, 'export_fields': []}
1179         for field in field_list:
1180             result['export_fields'].append((0, 0, {'name': field}))
1181         return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1182
1183     @openerpweb.jsonrequest
1184     def exist_export_lists(self, req, model):
1185         export_model = req.session.model("ir.exports")
1186         return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1187
1188     @openerpweb.jsonrequest
1189     def delete_export(self, req, export_id):
1190         req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1191         return True
1192
1193     @openerpweb.jsonrequest
1194     def namelist(self,req,  model, export_id):
1195
1196         result = self.get_data(req, model, req.session.eval_context(req.context))
1197         ir_export_obj = req.session.model("ir.exports")
1198         ir_export_line_obj = req.session.model("ir.exports.line")
1199
1200         field = ir_export_obj.read(export_id)
1201         fields = ir_export_line_obj.read(field['export_fields'])
1202
1203         name_list = {}
1204         [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1205         return name_list
1206
1207     def get_data(self, req, model, context=None):
1208         ids = []
1209         context = context or {}
1210         fields_data = {}
1211         proxy = req.session.model(model)
1212         fields = self.fields_get(req, model)
1213         if not ids:
1214             f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1215             f2 = proxy.fields_view_get(False, 'form', context)['fields']
1216
1217             fields = dict(f1)
1218             fields.update(f2)
1219             fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1220
1221         def rec(fields):
1222             _fields = {'id': 'ID' , '.id': 'Database ID' }
1223             def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1224                 fields_order = fields.keys()
1225                 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1226
1227                 for field in fields_order:
1228                     fields_data[prefix_node+field] = fields[field]
1229                     if prefix_node:
1230                         fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1231                     st_name = fields[field]['string'] or field
1232                     _fields[prefix_node+field] = st_name
1233                     if fields[field].get('relation', False) and level>0:
1234                         fields2 = self.fields_get(req,  fields[field]['relation'])
1235                         fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1236                         model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1237             model_populate(fields)
1238             return _fields
1239         return rec(fields)
1240
1241     @openerpweb.jsonrequest
1242     def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1243         context = req.session.eval_context(req.context)
1244         modle_obj = req.session.model(model)
1245         ids = ids or modle_obj.search(domain, context=context)
1246
1247         field = fields.keys()
1248         result = modle_obj.export_data(ids, field , context).get('datas',[])
1249
1250         if not import_compat:
1251             field = [val.strip() for val in fields.values()]
1252
1253         if export_format == 'xls':
1254             return export_xls(field, result)
1255         else:
1256             return export_csv(field, result)