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