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