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