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