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