[MERGE] project_issue: days since creation date added
[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, context=context))
394             for id, name, action in actions]
395
396 def clean_action(action, session, context=None):
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(context=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
563         context, domain = eval_context_and_domain(
564             request.session, request.context, domain)
565
566         ids = Model.search(domain, 0, False, sort or False, context)
567         # need to fill the dataset with all ids for the (domain, context) pair,
568         # so search un-paginated and paginate manually before reading
569         paginated_ids = ids[offset:(offset + limit if limit else None)]
570         if fields and fields == ['id']:
571             # shortcut read if we only want the ids
572             return {
573                 'ids': ids,
574                 'records': map(lambda id: {'id': id}, paginated_ids)
575             }
576
577         records = Model.read(paginated_ids, fields or False, context)
578         records.sort(key=lambda obj: ids.index(obj['id']))
579         return {
580             'ids': ids,
581             'records': records
582         }
583
584
585     @openerpweb.jsonrequest
586     def read(self, request, model, ids, fields=False):
587         return self.do_search_read(request, model, ids, fields)
588
589     @openerpweb.jsonrequest
590     def get(self, request, model, ids, fields=False):
591         return self.do_get(request, model, ids, fields)
592
593     def do_get(self, request, model, ids, fields=False):
594         """ Fetches and returns the records of the model ``model`` whose ids
595         are in ``ids``.
596
597         The results are in the same order as the inputs, but elements may be
598         missing (if there is no record left for the id)
599
600         :param request: the JSON-RPC2 request object
601         :type request: openerpweb.JsonRequest
602         :param model: the model to read from
603         :type model: str
604         :param ids: a list of identifiers
605         :type ids: list
606         :param fields: a list of fields to fetch, ``False`` or empty to fetch
607                        all fields in the model
608         :type fields: list | False
609         :returns: a list of records, in the same order as the list of ids
610         :rtype: list
611         """
612         Model = request.session.model(model)
613         records = Model.read(ids, fields, request.session.eval_context(request.context))
614
615         record_map = dict((record['id'], record) for record in records)
616
617         return [record_map[id] for id in ids if record_map.get(id)]
618
619     @openerpweb.jsonrequest
620     def load(self, req, model, id, fields):
621         m = req.session.model(model)
622         value = {}
623         r = m.read([id], False, req.session.eval_context(req.context))
624         if r:
625             value = r[0]
626         return {'value': value}
627
628     @openerpweb.jsonrequest
629     def create(self, req, model, data):
630         m = req.session.model(model)
631         r = m.create(data, req.session.eval_context(req.context))
632         return {'result': r}
633
634     @openerpweb.jsonrequest
635     def save(self, req, model, id, data):
636         m = req.session.model(model)
637         r = m.write([id], data, req.session.eval_context(req.context))
638         return {'result': r}
639
640     @openerpweb.jsonrequest
641     def unlink(self, request, model, ids=()):
642         Model = request.session.model(model)
643         return Model.unlink(ids, request.session.eval_context(request.context))
644
645     def call_common(self, req, model, method, args, domain_id=None, context_id=None):
646         domain = args[domain_id] if domain_id and len(args) - 1 >= domain_id  else []
647         context = args[context_id] if context_id and len(args) - 1 >= context_id  else {}
648         c, d = eval_context_and_domain(req.session, context, domain)
649         if domain_id and len(args) - 1 >= domain_id:
650             args[domain_id] = d
651         if context_id and len(args) - 1 >= context_id:
652             args[context_id] = c
653
654         return getattr(req.session.model(model), method)(*args)
655
656     @openerpweb.jsonrequest
657     def call(self, req, model, method, args, domain_id=None, context_id=None):
658         return self.call_common(req, model, method, args, domain_id, context_id)
659
660     @openerpweb.jsonrequest
661     def call_button(self, req, model, method, args, domain_id=None, context_id=None):
662         action = self.call_common(req, model, method, args, domain_id, context_id)
663         if isinstance(action, dict) and action.get('type') != '':
664             return {'result': clean_action(action, req.session)}
665         return {'result': False}
666
667     @openerpweb.jsonrequest
668     def exec_workflow(self, req, model, id, signal):
669         r = req.session.exec_workflow(model, id, signal)
670         return {'result': r}
671
672     @openerpweb.jsonrequest
673     def default_get(self, req, model, fields):
674         Model = req.session.model(model)
675         return Model.default_get(fields, req.session.eval_context(req.context))
676
677     @openerpweb.jsonrequest
678     def name_search(self, req, model, search_str, domain=[], context={}):
679         m = req.session.model(model)
680         r = m.name_search(search_str+'%', domain, '=ilike', context)
681         return {'result': r}
682
683 class DataGroup(openerpweb.Controller):
684     _cp_path = "/base/group"
685     @openerpweb.jsonrequest
686     def read(self, request, model, fields, group_by_fields, domain=None, sort=None):
687         Model = request.session.model(model)
688         context, domain = eval_context_and_domain(request.session, request.context, domain)
689
690         return Model.read_group(
691             domain or [], fields, group_by_fields, 0, False,
692             dict(context, group_by=group_by_fields), sort or False)
693
694 class View(openerpweb.Controller):
695     _cp_path = "/base/view"
696
697     def fields_view_get(self, request, model, view_id, view_type,
698                         transform=True, toolbar=False, submenu=False):
699         Model = request.session.model(model)
700         context = request.session.eval_context(request.context)
701         fvg = Model.fields_view_get(view_id, view_type, context, toolbar, submenu)
702         # todo fme?: check that we should pass the evaluated context here
703         self.process_view(request.session, fvg, context, transform)
704         return fvg
705
706     def process_view(self, session, fvg, context, transform):
707         # depending on how it feels, xmlrpclib.ServerProxy can translate
708         # XML-RPC strings to ``str`` or ``unicode``. ElementTree does not
709         # enjoy unicode strings which can not be trivially converted to
710         # strings, and it blows up during parsing.
711
712         # So ensure we fix this retardation by converting view xml back to
713         # bit strings.
714         if isinstance(fvg['arch'], unicode):
715             arch = fvg['arch'].encode('utf-8')
716         else:
717             arch = fvg['arch']
718
719         if transform:
720             evaluation_context = session.evaluation_context(context or {})
721             xml = self.transform_view(arch, session, evaluation_context)
722         else:
723             xml = ElementTree.fromstring(arch)
724         fvg['arch'] = Xml2Json.convert_element(xml)
725
726         for field in fvg['fields'].itervalues():
727             if field.get('views'):
728                 for view in field["views"].itervalues():
729                     self.process_view(session, view, None, transform)
730             if field.get('domain'):
731                 field["domain"] = self.parse_domain(field["domain"], session)
732             if field.get('context'):
733                 field["context"] = self.parse_context(field["context"], session)
734
735     @openerpweb.jsonrequest
736     def add_custom(self, request, view_id, arch):
737         CustomView = request.session.model('ir.ui.view.custom')
738         CustomView.create({
739             'user_id': request.session._uid,
740             'ref_id': view_id,
741             'arch': arch
742         }, request.session.eval_context(request.context))
743         return {'result': True}
744
745     @openerpweb.jsonrequest
746     def undo_custom(self, request, view_id, reset=False):
747         CustomView = request.session.model('ir.ui.view.custom')
748         context = request.session.eval_context(request.context)
749         vcustom = CustomView.search([('user_id', '=', request.session._uid), ('ref_id' ,'=', view_id)],
750                                     0, False, False, context)
751         if vcustom:
752             if reset:
753                 CustomView.unlink(vcustom, context)
754             else:
755                 CustomView.unlink([vcustom[0]], context)
756             return {'result': True}
757         return {'result': False}
758
759     def transform_view(self, view_string, session, context=None):
760         # transform nodes on the fly via iterparse, instead of
761         # doing it statically on the parsing result
762         parser = ElementTree.iterparse(StringIO(view_string), events=("start",))
763         root = None
764         for event, elem in parser:
765             if event == "start":
766                 if root is None:
767                     root = elem
768                 self.parse_domains_and_contexts(elem, session)
769         return root
770
771     def parse_domain(self, domain, session):
772         """ Parses an arbitrary string containing a domain, transforms it
773         to either a literal domain or a :class:`openerpweb.nonliterals.Domain`
774
775         :param domain: the domain to parse, if the domain is not a string it
776                        is assumed to be a literal domain and is returned as-is
777         :param session: Current OpenERP session
778         :type session: openerpweb.openerpweb.OpenERPSession
779         """
780         if not isinstance(domain, (str, unicode)):
781             return domain
782         try:
783             return openerpweb.ast.literal_eval(domain)
784         except ValueError:
785             # not a literal
786             return openerpweb.nonliterals.Domain(session, domain)
787
788     def parse_context(self, context, session):
789         """ Parses an arbitrary string containing a context, transforms it
790         to either a literal context or a :class:`openerpweb.nonliterals.Context`
791
792         :param context: the context to parse, if the context is not a string it
793                is assumed to be a literal domain and is returned as-is
794         :param session: Current OpenERP session
795         :type session: openerpweb.openerpweb.OpenERPSession
796         """
797         if not isinstance(context, (str, unicode)):
798             return context
799         try:
800             return openerpweb.ast.literal_eval(context)
801         except ValueError:
802             return openerpweb.nonliterals.Context(session, context)
803
804     def parse_domains_and_contexts(self, elem, session):
805         """ Converts domains and contexts from the view into Python objects,
806         either literals if they can be parsed by literal_eval or a special
807         placeholder object if the domain or context refers to free variables.
808
809         :param elem: the current node being parsed
810         :type param: xml.etree.ElementTree.Element
811         :param session: OpenERP session object, used to store and retrieve
812                         non-literal objects
813         :type session: openerpweb.openerpweb.OpenERPSession
814         """
815         for el in ['domain', 'filter_domain']:
816             domain = elem.get(el, '').strip()
817             if domain:
818                 elem.set(el, self.parse_domain(domain, session))
819         for el in ['context', 'default_get']:
820             context_string = elem.get(el, '').strip()
821             if context_string:
822                 elem.set(el, self.parse_context(context_string, session))
823
824 class FormView(View):
825     _cp_path = "/base/formview"
826
827     @openerpweb.jsonrequest
828     def load(self, req, model, view_id, toolbar=False):
829         fields_view = self.fields_view_get(req, model, view_id, 'form', toolbar=toolbar)
830         return {'fields_view': fields_view}
831
832 class ListView(View):
833     _cp_path = "/base/listview"
834
835     @openerpweb.jsonrequest
836     def load(self, req, model, view_id, toolbar=False):
837         fields_view = self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
838         return {'fields_view': fields_view}
839
840     def process_colors(self, view, row, context):
841         colors = view['arch']['attrs'].get('colors')
842
843         if not colors:
844             return None
845
846         color = [
847             pair.split(':')[0]
848             for pair in colors.split(';')
849             if eval(pair.split(':')[1], dict(context, **row))
850         ]
851
852         if not color:
853             return None
854         elif len(color) == 1:
855             return color[0]
856         return 'maroon'
857
858 class SearchView(View):
859     _cp_path = "/base/searchview"
860
861     @openerpweb.jsonrequest
862     def load(self, req, model, view_id):
863         fields_view = self.fields_view_get(req, model, view_id, 'search')
864         return {'fields_view': fields_view}
865
866     @openerpweb.jsonrequest
867     def fields_get(self, req, model):
868         Model = req.session.model(model)
869         fields = Model.fields_get(False, req.session.eval_context(req.context))
870         for field in fields.values():
871             # shouldn't convert the views too?
872             if field.get('domain'):
873                 field["domain"] = self.parse_domain(field["domain"], req.session)
874             if field.get('context'):
875                 field["context"] = self.parse_domain(field["context"], req.session)
876         return {'fields': fields}
877     
878     @openerpweb.jsonrequest
879     def get_filters(self, req, model):
880         Model = req.session.model("ir.filters")
881         filters = Model.get_filters(model)
882         for filter in filters:
883             filter["context"] = req.session.eval_context(self.parse_context(filter["context"], req.session))
884             filter["domain"] = req.session.eval_domain(self.parse_domain(filter["domain"], req.session))
885         return filters
886     
887     @openerpweb.jsonrequest
888     def save_filter(self, req, model, name, context_to_save, domain):
889         Model = req.session.model("ir.filters")
890         ctx = openerpweb.nonliterals.CompoundContext(context_to_save)
891         ctx.session = req.session
892         ctx = ctx.evaluate()
893         domain = openerpweb.nonliterals.CompoundDomain(domain)
894         domain.session = req.session
895         domain = domain.evaluate()
896         uid = req.session._uid
897         context = req.session.eval_context(req.context)
898         to_return = Model.create_or_replace({"context": ctx,
899                                              "domain": domain,
900                                              "model_id": model,
901                                              "name": name,
902                                              "user_id": uid
903                                              }, context)
904         return to_return
905
906 class Binary(openerpweb.Controller):
907     _cp_path = "/base/binary"
908
909     @openerpweb.httprequest
910     def image(self, request, session_id, model, id, field, **kw):
911         cherrypy.response.headers['Content-Type'] = 'image/png'
912         Model = request.session.model(model)
913         context = request.session.eval_context(request.context)
914         try:
915             if not id:
916                 res = Model.default_get([field], context).get(field, '')
917             else:
918                 res = Model.read([int(id)], [field], context)[0].get(field, '')
919             return base64.decodestring(res)
920         except: # TODO: what's the exception here?
921             return self.placeholder()
922     def placeholder(self):
923         return open(os.path.join(openerpweb.path_addons, 'base', 'static', 'src', 'img', 'placeholder.png'), 'rb').read()
924
925     @openerpweb.httprequest
926     def saveas(self, request, session_id, model, id, field, fieldname, **kw):
927         Model = request.session.model(model)
928         context = request.session.eval_context(request.context)
929         res = Model.read([int(id)], [field, fieldname], context)[0]
930         filecontent = res.get(field, '')
931         if not filecontent:
932             raise cherrypy.NotFound
933         else:
934             cherrypy.response.headers['Content-Type'] = 'application/octet-stream'
935             filename = '%s_%s' % (model.replace('.', '_'), id)
936             if fieldname:
937                 filename = res.get(fieldname, '') or filename
938             cherrypy.response.headers['Content-Disposition'] = 'attachment; filename=' +  filename
939             return base64.decodestring(filecontent)
940
941     @openerpweb.httprequest
942     def upload(self, request, session_id, callback, ufile=None):
943         cherrypy.response.timeout = 500
944         headers = {}
945         for key, val in cherrypy.request.headers.iteritems():
946             headers[key.lower()] = val
947         size = int(headers.get('content-length', 0))
948         # TODO: might be useful to have a configuration flag for max-length file uploads
949         try:
950             out = """<script language="javascript" type="text/javascript">
951                         var win = window.top.window,
952                             callback = win[%s];
953                         if (typeof(callback) === 'function') {
954                             callback.apply(this, %s);
955                         } else {
956                             win.jQuery('#oe_notification', win.document).notify('create', {
957                                 title: "Ajax File Upload",
958                                 text: "Could not find callback"
959                             });
960                         }
961                     </script>"""
962             data = ufile.file.read()
963             args = [size, ufile.filename, ufile.headers.getheader('Content-Type'), base64.encodestring(data)]
964         except Exception, e:
965             args = [False, e.message]
966         return out % (simplejson.dumps(callback), simplejson.dumps(args))
967
968     @openerpweb.httprequest
969     def upload_attachment(self, request, session_id, callback, model, id, ufile=None):
970         cherrypy.response.timeout = 500
971         context = request.session.eval_context(request.context)
972         Model = request.session.model('ir.attachment')
973         try:
974             out = """<script language="javascript" type="text/javascript">
975                         var win = window.top.window,
976                             callback = win[%s];
977                         if (typeof(callback) === 'function') {
978                             callback.call(this, %s);
979                         }
980                     </script>"""
981             attachment_id = Model.create({
982                 'name': ufile.filename,
983                 'datas': base64.encodestring(ufile.file.read()),
984                 'res_model': model,
985                 'res_id': int(id)
986             }, context)
987             args = {
988                 'filename': ufile.filename,
989                 'id':  attachment_id
990             }
991         except Exception, e:
992             args = { 'error': e.message }
993         return out % (simplejson.dumps(callback), simplejson.dumps(args))
994
995 class Action(openerpweb.Controller):
996     _cp_path = "/base/action"
997
998     @openerpweb.jsonrequest
999     def load(self, req, action_id):
1000         Actions = req.session.model('ir.actions.actions')
1001         value = False
1002         context = req.session.eval_context(req.context)
1003         action_type = Actions.read([action_id], ['type'], context)
1004         if action_type:
1005             action = req.session.model(action_type[0]['type']).read([action_id], False,
1006                                                                     context)
1007             if action:
1008                 value = clean_action(action[0], req.session)
1009         return {'result': value}
1010
1011     @openerpweb.jsonrequest
1012     def run(self, req, action_id):
1013         return clean_action(req.session.model('ir.actions.server').run(
1014             [action_id], req.session.eval_context(req.context)), req.session)
1015
1016 class TreeView(View):
1017     _cp_path = "/base/treeview"
1018
1019     @openerpweb.jsonrequest
1020     def load(self, req, model, view_id, toolbar=False):
1021         return self.fields_view_get(req, model, view_id, 'tree', toolbar=toolbar)
1022
1023     @openerpweb.jsonrequest
1024     def action(self, req, model, id):
1025         return load_actions_from_ir_values(
1026             req,'action', 'tree_but_open',[(model, id)],
1027             False, req.session.eval_context(req.context))
1028
1029 def export_csv(fields, result):
1030     fp = StringIO()
1031     writer = csv.writer(fp, quoting=csv.QUOTE_ALL)
1032
1033     writer.writerow(fields)
1034
1035     for data in result:
1036         row = []
1037         for d in data:
1038             if isinstance(d, basestring):
1039                 d = d.replace('\n',' ').replace('\t',' ')
1040                 try:
1041                     d = d.encode('utf-8')
1042                 except:
1043                     pass
1044             if d is False: d = None
1045             row.append(d)
1046         writer.writerow(row)
1047
1048     fp.seek(0)
1049     data = fp.read()
1050     fp.close()
1051     return data
1052
1053 def export_xls(fieldnames, table):
1054     try:
1055         import xlwt
1056     except ImportError:
1057         common.error(_('Import Error.'), _('Please install xlwt library to export to MS Excel.'))
1058
1059     workbook = xlwt.Workbook()
1060     worksheet = workbook.add_sheet('Sheet 1')
1061
1062     for i, fieldname in enumerate(fieldnames):
1063         worksheet.write(0, i, str(fieldname))
1064         worksheet.col(i).width = 8000 # around 220 pixels
1065
1066     style = xlwt.easyxf('align: wrap yes')
1067
1068     for row_index, row in enumerate(table):
1069         for cell_index, cell_value in enumerate(row):
1070             cell_value = str(cell_value)
1071             cell_value = re.sub("\r", " ", cell_value)
1072             worksheet.write(row_index + 1, cell_index, cell_value, style)
1073
1074
1075     fp = StringIO()
1076     workbook.save(fp)
1077     fp.seek(0)
1078     data = fp.read()
1079     fp.close()
1080     #return data.decode('ISO-8859-1')
1081     return unicode(data, 'utf-8', 'replace')
1082
1083 class Export(View):
1084     _cp_path = "/base/export"
1085
1086     def fields_get(self, req, model):
1087         Model = req.session.model(model)
1088         fields = Model.fields_get(False, req.session.eval_context(req.context))
1089         return fields
1090
1091     @openerpweb.jsonrequest
1092     def get_fields(self, req, model, prefix='', name= '', field_parent=None, params={}):
1093         import_compat = params.get("import_compat", False)
1094
1095         fields = self.fields_get(req, model)
1096         field_parent_type = params.get("parent_field_type",False)
1097
1098         if import_compat and field_parent_type and field_parent_type == "many2one":
1099             fields = {}
1100
1101         fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1102         records = []
1103         fields_order = fields.keys()
1104         fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1105
1106         for index, field in enumerate(fields_order):
1107             value = fields[field]
1108             record = {}
1109             if import_compat and value.get('readonly', False):
1110                 ok = False
1111                 for sl in value.get('states', {}).values():
1112                     for s in sl:
1113                         ok = ok or (s==['readonly',False])
1114                 if not ok: continue
1115
1116             id = prefix + (prefix and '/'or '') + field
1117             nm = name + (name and '/' or '') + value['string']
1118             record.update(id=id, string= nm, action='javascript: void(0)',
1119                           target=None, icon=None, children=[], field_type=value.get('type',False), required=value.get('required', False))
1120             records.append(record)
1121
1122             if len(nm.split('/')) < 3 and value.get('relation', False):
1123                 if import_compat:
1124                     ref = value.pop('relation')
1125                     cfields = self.fields_get(req, ref)
1126                     if (value['type'] == 'many2many'):
1127                         record['children'] = []
1128                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1129
1130                     elif value['type'] == 'many2one':
1131                         record['children'] = [id + '/id', id + '/.id']
1132                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1133
1134                     else:
1135                         cfields_order = cfields.keys()
1136                         cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1137                         children = []
1138                         for j, fld in enumerate(cfields_order):
1139                             cid = id + '/' + fld
1140                             cid = cid.replace(' ', '_')
1141                             children.append(cid)
1142                         record['children'] = children or []
1143                         record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1144                 else:
1145                     ref = value.pop('relation')
1146                     cfields = self.fields_get(req, ref)
1147                     cfields_order = cfields.keys()
1148                     cfields_order.sort(lambda x,y: -cmp(cfields[x].get('string', ''), cfields[y].get('string', '')))
1149                     children = []
1150                     for j, fld in enumerate(cfields_order):
1151                         cid = id + '/' + fld
1152                         cid = cid.replace(' ', '_')
1153                         children.append(cid)
1154                     record['children'] = children or []
1155                     record['params'] = {'model': ref, 'prefix': id, 'name': nm}
1156
1157         records.reverse()
1158         return records
1159
1160     @openerpweb.jsonrequest
1161     def save_export_lists(self, req, name, model, field_list):
1162         result = {'resource':model, 'name':name, 'export_fields': []}
1163         for field in field_list:
1164             result['export_fields'].append((0, 0, {'name': field}))
1165         return req.session.model("ir.exports").create(result, req.session.eval_context(req.context))
1166
1167     @openerpweb.jsonrequest
1168     def exist_export_lists(self, req, model):
1169         export_model = req.session.model("ir.exports")
1170         return export_model.read(export_model.search([('resource', '=', model)]), ['name'])
1171
1172     @openerpweb.jsonrequest
1173     def delete_export(self, req, export_id):
1174         req.session.model("ir.exports").unlink(export_id, req.session.eval_context(req.context))
1175         return True
1176
1177     @openerpweb.jsonrequest
1178     def namelist(self,req,  model, export_id):
1179
1180         result = self.get_data(req, model, req.session.eval_context(req.context))
1181         ir_export_obj = req.session.model("ir.exports")
1182         ir_export_line_obj = req.session.model("ir.exports.line")
1183
1184         field = ir_export_obj.read(export_id)
1185         fields = ir_export_line_obj.read(field['export_fields'])
1186
1187         name_list = {}
1188         [name_list.update({field['name']: result.get(field['name'])}) for field in fields]
1189         return name_list
1190
1191     def get_data(self, req, model, context=None):
1192         ids = []
1193         context = context or {}
1194         fields_data = {}
1195         proxy = req.session.model(model)
1196         fields = self.fields_get(req, model)
1197         if not ids:
1198             f1 = proxy.fields_view_get(False, 'tree', context)['fields']
1199             f2 = proxy.fields_view_get(False, 'form', context)['fields']
1200
1201             fields = dict(f1)
1202             fields.update(f2)
1203             fields.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1204
1205         def rec(fields):
1206             _fields = {'id': 'ID' , '.id': 'Database ID' }
1207             def model_populate(fields, prefix_node='', prefix=None, prefix_value='', level=2):
1208                 fields_order = fields.keys()
1209                 fields_order.sort(lambda x,y: -cmp(fields[x].get('string', ''), fields[y].get('string', '')))
1210
1211                 for field in fields_order:
1212                     fields_data[prefix_node+field] = fields[field]
1213                     if prefix_node:
1214                         fields_data[prefix_node + field]['string'] = '%s%s' % (prefix_value, fields_data[prefix_node + field]['string'])
1215                     st_name = fields[field]['string'] or field
1216                     _fields[prefix_node+field] = st_name
1217                     if fields[field].get('relation', False) and level>0:
1218                         fields2 = self.fields_get(req,  fields[field]['relation'])
1219                         fields2.update({'id': {'string': 'ID'}, '.id': {'string': 'Database ID'}})
1220                         model_populate(fields2, prefix_node+field+'/', None, st_name+'/', level-1)
1221             model_populate(fields)
1222             return _fields
1223         return rec(fields)
1224
1225     @openerpweb.jsonrequest
1226     def export_data(self, req, model, fields, ids, domain, import_compat=False, export_format="csv", context=None):
1227         context = req.session.eval_context(req.context)
1228         modle_obj = req.session.model(model)
1229         ids = ids or modle_obj.search(domain, context=context)
1230
1231         field = fields.keys()
1232         result = modle_obj.export_data(ids, field , context).get('datas',[])
1233
1234         if not import_compat:
1235             field = [val.strip() for val in fields.values()]
1236
1237         if export_format == 'xls':
1238             return export_xls(field, result)
1239         else:
1240             return export_csv(field, result)