948422e8296cfc440aef97592a9d31d7eb067756
[odoo/odoo.git] / addons / website / models / ir_qweb.py
1 # -*- coding: utf-8 -*-
2 """
3 Website-context rendering needs to add some metadata to rendered fields,
4 as well as render a few fields differently.
5
6 Also, adds methods to convert values back to openerp models.
7 """
8
9 import cStringIO
10 import itertools
11 import logging
12 import re
13 import urllib2
14
15 import werkzeug.utils
16 from lxml import etree, html
17 from PIL import Image as I
18
19 from openerp.osv import orm, fields
20 from openerp.tools import ustr
21
22 REMOTE_CONNECTION_TIMEOUT = 2.5
23
24 logger = logging.getLogger(__name__)
25
26 class QWeb(orm.AbstractModel):
27     """ QWeb object for rendering stuff in the website context
28     """
29     _name = 'website.qweb'
30     _inherit = 'ir.qweb'
31
32     def get_converter_for(self, field_type):
33         return self.pool.get(
34             'website.qweb.field.' + field_type,
35             self.pool['website.qweb.field'])
36
37 class Field(orm.AbstractModel):
38     _name = 'website.qweb.field'
39     _inherit = 'ir.qweb.field'
40
41     def attributes(self, cr, uid, field_name, record, options,
42                    source_element, g_att, t_att, qweb_context, context=None):
43         column = record._model._all_columns[field_name].column
44         return itertools.chain(
45             super(Field, self).attributes(cr, uid, field_name, record, options,
46                                           source_element, g_att, t_att,
47                                           qweb_context, context=context),
48             [('data-oe-translate', 1 if column.translate else 0)]
49         )
50
51     def value_from_string(self, value):
52         return value
53
54     def from_html(self, cr, uid, model, column, element, context=None):
55         return self.value_from_string(element.text_content().strip())
56
57     def qweb_object(self):
58         return self.pool['website.qweb']
59
60 class Integer(orm.AbstractModel):
61     _name = 'website.qweb.field.integer'
62     _inherit = ['website.qweb.field']
63
64     value_from_string = int
65
66 class Float(orm.AbstractModel):
67     _name = 'website.qweb.field.float'
68     _inherit = ['website.qweb.field', 'ir.qweb.field.float']
69
70     def from_html(self, cr, uid, model, column, element, context=None):
71         lang = self.user_lang(cr, uid, context=context)
72
73         value = element.text_content().strip()
74
75         return float(value.replace(lang.thousands_sep, '')
76                           .replace(lang.decimal_point, '.'))
77
78 class Date(orm.AbstractModel):
79     _name = 'website.qweb.field.date'
80     _inherit = ['website.qweb.field', 'ir.qweb.field.date']
81
82     def from_html(self, cr, uid, model, column, element, context=None):
83         raise NotImplementedError("Can not parse and save localized dates")
84
85 class DateTime(orm.AbstractModel):
86     _name = 'website.qweb.field.datetime'
87     _inherit = ['website.qweb.field', 'ir.qweb.field.datetime']
88
89     def from_html(self, cr, uid, model, column, element, context=None):
90         raise NotImplementedError("Can not parse and save localized datetimes")
91
92 class Text(orm.AbstractModel):
93     _name = 'website.qweb.field.text'
94     _inherit = ['website.qweb.field', 'ir.qweb.field.text']
95
96     def from_html(self, cr, uid, model, column, element, context=None):
97         return element.text_content()
98
99 class Selection(orm.AbstractModel):
100     _name = 'website.qweb.field.selection'
101     _inherit = ['website.qweb.field', 'ir.qweb.field.selection']
102
103     def from_html(self, cr, uid, model, column, element, context=None):
104         value = element.text_content().strip()
105         selection = column.reify(cr, uid, model, column, context=context)
106         for k, v in selection:
107             if isinstance(v, str):
108                 v = ustr(v)
109             if value == v:
110                 return k
111
112         raise ValueError(u"No value found for label %s in selection %s" % (
113                          value, selection))
114
115 class ManyToOne(orm.AbstractModel):
116     _name = 'website.qweb.field.many2one'
117     _inherit = ['website.qweb.field', 'ir.qweb.field.many2one']
118
119     def from_html(self, cr, uid, model, column, element, context=None):
120         # FIXME: this behavior is really weird, what if the user wanted to edit the name of the related thingy? Should m2os really be editable without a widget?
121         matches = self.pool[column._obj].name_search(
122             cr, uid, name=element.text_content().strip(), context=context)
123         # FIXME: no match? More than 1 match?
124         assert len(matches) == 1
125         return matches[0][0]
126
127 class HTML(orm.AbstractModel):
128     _name = 'website.qweb.field.html'
129     _inherit = ['website.qweb.field', 'ir.qweb.field.html']
130
131     def from_html(self, cr, uid, model, column, element, context=None):
132         content = []
133         if element.text: content.append(element.text)
134         content.extend(html.tostring(child)
135                        for child in element.iterchildren(tag=etree.Element))
136         return '\n'.join(content)
137
138
139 class Image(orm.AbstractModel):
140     """
141     Widget options:
142
143     ``class``
144         set as attribute on the generated <img> tag
145     """
146     _name = 'website.qweb.field.image'
147     _inherit = ['website.qweb.field', 'ir.qweb.field.image']
148
149     def to_html(self, cr, uid, field_name, record, options,
150                 source_element, t_att, g_att, qweb_context, context=None):
151         assert source_element.nodeName != 'img',\
152             "Oddly enough, the root tag of an image field can not be img. " \
153             "That is because the image goes into the tag, or it gets the " \
154             "hose again."
155
156         return super(Image, self).to_html(
157             cr, uid, field_name, record, options,
158             source_element, t_att, g_att, qweb_context, context=context)
159
160     def record_to_html(self, cr, uid, field_name, record, column, options=None, context=None):
161         cls = ''
162         if 'class' in options:
163             cls = ' class="%s"' % werkzeug.utils.escape(options['class'])
164
165         return '<img%s src="/website/image?model=%s&field=%s&id=%s"/>' % (
166             cls, record._model._name, field_name, record.id)
167
168     def from_html(self, cr, uid, model, column, element, context=None):
169         url = element.find('img').get('src')
170
171         m = re.match(r'^/website/attachment/(\d+)$', url)
172         if m:
173             attachment = self.pool['ir.attachment'].browse(
174                 cr, uid, int(m.group(1)), context=context)
175             return attachment.datas
176
177         # remote URL?
178         try:
179             # should probably remove remote URLs entirely:
180             # * in fields, downloading them without blowing up the server is a
181             #   challenge
182             # * in views, may trigger mixed content warnings if HTTPS CMS
183             #   linking to HTTP images
184             # implement drag & drop image upload to mitigate?
185
186             req = urllib2.urlopen(url, timeout=REMOTE_CONNECTION_TIMEOUT)
187             # PIL needs a seekable file-like image, urllib result is not seekable
188             image = I.open(cStringIO.StringIO(req.read()))
189             # force a complete load of the image data to validate it
190             image.load()
191         except Exception:
192             logger.exception("Failed to load remote image %r", url)
193             return False
194
195         # don't use original data in case weird stuff was smuggled in, with
196         # luck PIL will remove some of it?
197         out = cStringIO.StringIO()
198         image.save(out, image.format)
199         return out.getvalue().encode('base64')
200
201 class Monetary(orm.AbstractModel):
202     _name = 'website.qweb.field.monetary'
203     _inherit = ['website.qweb.field', 'ir.qweb.field.monetary']
204
205     def from_html(self, cr, uid, model, column, element, context=None):
206         lang = self.user_lang(cr, uid, context=context)
207
208         value = element.find('span').text.strip()
209
210         return float(value.replace(lang.thousands_sep, '')
211                           .replace(lang.decimal_point, '.'))