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