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