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