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